WEB
SecretVault
User(id=0, username='admin'),并为该用户插入了一个 VaultEntry,label='flag',密码字段是用 Fernet 加密的 flag(文件 /flag 的内容)。

只要以 id=0 的身份访问 dashboard,就能看到并解出 flag。
题目有一个中间件代理,使用Go的reverseproxy做代理转发接收的请求,然后发给flask,首先想到要伪造jwt,伪造一个uid为0的jwt token,但是没办法获取secretkey。另一种思路就是怎么让代理转发后的请求头没有X-User请求头,或者X-User请求头对应值为0,但是main.go中写死了,请求头中的X-User肯定会存在,要不为jwt解密出来的uid,要不为anonymous。
之后查看golang 的 reverseproxy 源码,发现如下代码片段
https://go.dev/src/net/http/httputil/reverseproxy.go
代码对应的功能是移除 HTTP 请求 / 响应中的 "逐跳头部"(Hop-by-Hop Headers),确保代理服务器(如反向代理、网关)在转发请求时符合 HTTP 协议规范,避免头部被不当传递到下游服务。
代码会遍历 Connection 头部的值(可能是逗号分隔的多个字段),然后分割逗号分隔的字段,之后删除该字段。
例如 Connection: keep-alive, X-Custom 表示 keep-alive 和 X-Custom 是逐跳头部,应被当前代理移除,不转发给下游。这段代码会解析 Connection 头部的值,将其中声明的所有字段从请求响应头中删除。
利用这个代理转发特性,将Connection头部值改成 Connection: close,x-user
然后就会将如下请求
GET /dashboard HTTP/1.1
Host: xxxxxxx
User-Agent: curl/7.68.0
Accept: */*
X-User: 1
Connection: close,x-user
修改成如下请求转发
GET /dashboard HTTP/1.1
Host: xxxxxxx
User-Agent: curl/7.68.0
Accept: */*
# 以下头部被移除:
# - Connection: close,x-user(标准逐跳头部,被删除)
# - X-User: 1(被 Connection 声明为逐跳,被删除)
之后flask收到的请求头不包含X-User: 1,默认将0赋值给uid,然后输出flag
ezphp
function generateRandomString($length = 8){
$characters = 'abcdefghijklmnopqrstuvwxyz';
$randomString = '';
for ($i = 0;$i < $length; $i++) {
$r = rand(0, strlen($characters) - 1);
$randomString .= $characters[$r];
}
return $randomString;
}
date_default_timezone_set('Asia/Shanghai');
class test{
public $readflag;
public $f;
public $key;
public function __construct(){
$this->readflag = new class {
public function __construct(){
if (isset($_FILES['file']) && $_FILES['file']['error'] == 0) {
$time = date('Hi');
$filename = $GLOBALS['filename'];
$seed = $time . intval($filename);
mt_srand($seed);
$uploadDir = 'uploads/';
$files = glob($uploadDir . '*');
foreach ($files as $file) {
if (is_file($file)) unlink($file);
}
$randomStr = generateRandomString(8);
$newFilename = $time . '.' . $randomStr . '.' . 'jpg';
$GLOBALS['file'] = $newFilename;
$uploadedFile = $_FILES['file']['tmp_name'];
$uploadPath = $uploadDir . $newFilename;
if (system("cp ".$uploadedFile." ". $uploadPath)) {
echo "success upload!";
} else {
echo "error";
}
}
}
public function __wakeup(){
phpinfo();
}
public function readflag(){
function readflag(){
if (isset($GLOBALS['file'])) {
$file = $GLOBALS['file'];
$file = basename($file);
if (preg_match('/:\/\//', $file))
die("error");
$file_content = file_get_contents("uploads/" . $file);
if (preg_match('/<\?|\:\/\/|ph|\?\=/i', $file_content)) {
die("Illegal content detected in the file.");
}
include("uploads/" . $file);
}
}
}
};
}
public function __destruct(){
$func = $this->f;
$GLOBALS['filename'] = $this->readflag;
if ($this->key == 'class')
new $func();
else if ($this->key == 'func') {
$func();
} else {
highlight_file('index.php');
}
}
}
$ser = isset($_GET['land']) ? $_GET['land'] : 'O:4:"test":N';
@unserialize($ser);

析构函数中可以直接调用函数,可以将$func赋值为函数名调用函数,也可以用数组回调对象方法,第一个参数为对象变量,第二个参数为类中的方法名称字符串。
然后看test类中的构造函数
新建了一个匿名类,赋值给$this->readflag。新的匿名类中包含一个构造函数,功能大致为可以向uploads文件夹任意上传文件,文件内容无限制,文件名称和后缀不可控。还包含一个readflag函数,其中又声明了readflag函数,主要功能,是先检查文件的内容,如果不包含危险字符,则包含对应的文件,本地测试发现,上传的文件在每分钟之内文件名固定,
那么就可以条件竞争,一边传执行命令的php脚本,一边传可通过过滤的文本。
执行test类的__construct函数,很好构造
这样就可以传任意内容的文件了。
之后就是要触发readflag函数,进行文件包含了。
exp中最后序列化的一个数组,依次分三个步骤,第一步,先去调用执行test类的__construct函数,然后得到test实例化后的对象,对应exp中的t变量,第二步,调用t变量,第二步,调用t变量,第二步,调用this->readflag的readflag方法,然后得到对readflag函数的声明,之后就可以调用readflag的方法,进行文件包含。构造好这3个对象,存于一个数组中,然后进行序列化,可执行readflag函数。
之后burp 条件竞争,先列目录

之后发现flag没权限读,需要提权,suid提权 find / -perm -u=s -type f 2>/dev/null
base64 可以提权
