Skip to content

Instantly share code, notes, and snippets.

@pome1618
Created May 16, 2020 21:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pome1618/45b301ed41e4a255f457d816484a9f1e to your computer and use it in GitHub Desktop.
Save pome1618/45b301ed41e4a255f457d816484a9f1e to your computer and use it in GitHub Desktop.
badsns2019 作業記録
【1.前提情報】
参加ページ[1-1]
GitHub[1-2]
[1-1] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
[1-2] https://github.com/ommadawn46/badsns2019
【2.BadSNSに対する調査】
"BadSNS"で検索をしたところ、Bad SNSはにしむねあさんが開発した診断練習用Webアプリケーションであることがわかった。
また、WEB+DB PRESS Vol.103にて詳細な解説があるらしい。書籍は購入していないが、目次から
・新規ユーザー登録画面に潜む脆弱性:OSコマンドインジェクション,ディレクトリトラバーサル
・ログイン画面とメイン画面に潜む脆弱性:SQLインジェクション,アクセス制御の不備,XSS
と記述されているため、そのあたりを探る。
なお、あるページ[2-1]より使用ツールとしてBrakemanが記述されているため最初にBrakemanを試し、次にVulsやMetasploitを試す。
追記:Vulsの結果とDockerfileより、Metasploitの優先度は下げる
[2-1] https://www.farend.co.jp/blog/2018/06/badsns/
【3.dockerの準備】
Dockerの実行環境は、最近Docker for Windowsがリリースされたためそちらを用いる。
DockerImageのダウンロードはGitHubのReadmeの通り、
$ docker pull ommadawn46/badsns2019
実行は
$ docker run --privileged -d --rm -p 10080:80 --name badsns2019 ommadawn46/badsns2019
である。また、動作はブラウザから
http://localhost:10080/ 及び http://localhost:10080/mailhog/ にアクセスすることで確認できる。
【4.Brakemanの実行】
$ docker pull ommadawn46/badsns2019
$ docker run --privileged -d --rm -p 10080:80 --name badsns2019 ommadawn46/badsns2019
にてDockerImageのダウンロード、実行を行う。その後、
$ docker ps
にて実行中のコンテナIDを控える。
$docker exec -it [コンテナID] bash
にて対象のbashに入る。ここからはBrakemanのインストールと実行だが、[4-1]を参考に
$ gem install brakeman
$ brakeman /var/www/app/sns/
とすると脆弱性の診断を行うことができる。
また、BrakemanのオプションとしてはAとw1があり、Aはすべての種類、w1はすべてのレベルを検査する。
$ brakeman /var/www/app/sns/ > brakeman.txt
$ brakeman -A -w1 /var/www/app/sns/ > brakeman_A_w1.txt
としてリダイレクトして保存した。
Security Warningsはオプション無しで14、オプションありで18となった。
詳細はbrakeman_A_w1.txtを参照
[4-1] https://blog-ja.sideci.com/entry/2017/07/06/110000
【5.Vulsの使用】
badsnsはUbuntu16の上に構成されているため、
Vulsを[5-1] [5-2]を参考に、インストールする
[Install requirements]
$ apt-get update
$ apt -y install sqlite git debian-goodies gcc make wget
$ wget https://dl.google.com/go/go1.14.2.linux-amd64.tar.gz
$ tar -C /usr/local -xzf go1.14.2.linux-amd64.tar.gz
$ mkdir $HOME/go
$ apt-get install vim
$ vim /etc/profile.d/goenv.sh
export GOROOT=/usr/local/go
export GOPATH=$HOME/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
$ source /etc/profile.d/goenv.sh
[Deploy go-cve-dictionary]
$ mkdir /var/log/vuls
$ chown root /var/log/vuls
$ chmod 700 /var/log/vuls
$ mkdir -p $GOPATH/src/github.com/kotakanbe
$ cd $GOPATH/src/github.com/kotakanbe
$ git clone https://github.com/kotakanbe/go-cve-dictionary.git
$ cd go-cve-dictionary
$ make install
$ cd $HOME
$ for i in `seq 2002 $(date +"%Y")`; do go-cve-dictionary fetchnvd -years $i; done
$ for i in `seq 1998 $(date +"%Y")`; do go-cve-dictionary fetchjvn -years $i; done
[Deploy goval-dictionary]
$ mkdir -p $GOPATH/src/github.com/kotakanbe
$ cd $GOPATH/src/github.com/kotakanbe
$ git clone https://github.com/kotakanbe/goval-dictionary.git
$ cd goval-dictionary
$ make install
$ ln -s $GOPATH/src/github.com/kotakanbe/goval-dictionary/oval.sqlite3 $HOME/oval.sqlite3
$ goval-dictionary fetch-ubuntu 16
[Deploy Vuls]
$ mkdir -p $GOPATH/src/github.com/future-architect
$ cd $GOPATH/src/github.com/future-architect
$ git clone https://github.com/future-architect/vuls.git
$ cd vuls
$ make install
[execute Vuls]
$ cd $HOME
$ vim config.toml
[servers]
[servers.localhost]
host = "localhost"
port = "local"
$ vuls configtest
$ vuls scan
$ vuls report -format-one-line-text
$ vuls report -format-full-text>vuls_format_full_text.txt
$ vuls report -format-full-text -ignore-unfixed > vuls_format_full_text_ignore_unfixed.txt
$ vuls tui
$ vuls tui -ignore-unfixed
[5-1] https://vuls.io/docs/ja/install-manually-centos.html
[5-2] https://www.virment.com/how-to-install-vuls-on-ubuntu/
【6.Vulsのリモートスキャン】
[6-1]からCentOS8.1.1911のISOイメージをダウンロードし、Hyper-Vマネージャで仮想マシンを新規作成しインストール。
その後、先程と同様に公式サイトに従いVulsのインストールを行う。
診断対象の対象の仮想マシンはそのままではコンテナの22番ポートにアクセスできないため、
$ docker run --privileged -d --rm -p 10080:80 -p 10022:22 --name badsns2019 ommadawn46/badsns2019
としてlocalhost:10022でコンテナの22番ポートにアクセスできるようにする。
$ apt-get update
$ apt-get install openssh-server vim
$ /etc/init.d/ssh start
$ vim /etc/ssh/sshd_config
PermitRootLoginをyesへ
$ ssh localhost #ssh接続確認
ここでホストからTera Termでsshの疎通確認
また、Vuls用仮想マシンからも疎通確認
疎通確認が取れたあとは鍵生成を行い、Tera Termで公開鍵を貼り付けて鍵認証を使えるようにする
Vuls用仮想マシンにて
$ ssh-keygen -t rsa
$ cat ~/.ssh/id_rsa.pub
としてコピー
診断対象にて
$ mkdir ~/.ssh
$ chmod 700 ~/.ssh
$ touch ~/.ssh/authorized_keys
$ chmod 600 ~/.ssh/authorized_keys
$ vim ~/.ssh/authorized_keys
として~/.ssh/authorized_keysにペースト
次にVuls用仮想マシンにて
$ ssh -p 10022 root@192.168.123.7 -i ~/.ssh/id_rsa
$ cd $HOME
$ cat config.toml
[servers]
[servers.ubuntu]
host = "192.168.123.7"
port = "10022"
user = "root"
keyPath = "/root/.ssh/id_rsa"
$ vuls configtest ubuntu
$ vuls scan ubuntu
今回はTUIで十分情報は閲覧できるので、
$ vuls tui
$ vuls tui -ignore-unfixed
として情報を見る
また、ログはvuls_report_format_full_text.txtなどを参照。
[6-1] https://www.centos.org/download/
【7.Burp Suiteによる検証】
Burp Suiteによってフォワードプロキシを構成し、パラメータの書き換えを行う
インストールは[7-1]からダウンロードし、インストールする。
起動したら[Temporary project]から[Use Burp defaults]とする。
[Proxy]タブから[Options]を選択し、デフォルトで設定されているProxy Listenersのポート番号を20080に変更する。
また、PCのプロキシ設定からプロキシサーバを127.0.0.1:20080とする。
この状態でbadsns2019へのアクセス経路は以下のようになる。
ブラウザ---127.0.0.1:20080---[Burp Suite]---127.0.0.1:10080---badsns2019:80
Brakemanにより発覚した脆弱性より、icon登録時に渡されるresize_max_pixelパラメータに数字ではなくコマンド(e.g.";touch aaa;")を渡すと、
WorkingDirectory(/var/www/app/sns/)にて実行される。
実際に新規ユーザ登録画面にてアイコンをアップロードする際に、Burp Suiteでresize_max_pixelパラメータに";touch aaa;"を渡したところ、
/var/www/app/sns/にaaaが作成されていることを確認した。
[7-1] https://portswigger.net/burp/releases/professional-community-2020-4
【8.OSコマンドインジェクションの対策】
修正方法としてはpxには数値のみが格納されるように、
px = params[:resize_max_pixel].to_i
とし、それに伴い5行目のif文に
params[:resize_max_pixel].to_i > 0
を加える。
if px = params[:resize_max_pixel].to_i and params[:resize_max_pixel].to_i > 0
また、dest_file_nameに使用されるparams[:image]はpng|jpeg|jpg|gifに制限したほうが安全そう
そのため、5行目のif文について
File.extname(file_name).match(/png|jpeg|jpg|gif/)
File.extname(file_name).match(/^\.png$|^\.jpeg$|^\.jpg$|^\.gif$/)
とする。
Brakemanにより発覚したopen(params[:url])についての脆弱性より、攻撃可能であることは検証できなかったものの、
break unless URI.parse(params[:url]) and page = open(params[:url]) rescue nil
としてバリデーションを行った
【9.SQLインジェクションに対するプレースホルダの使用による対策】
Brakemanで発見されたSQLインジェクションについて、各所でプレースホルダーを使用する
app/controllers/friends_controller.rb:16
if User.column_names.include?(order.split(' ',2)[0]) and order.split(' ',2)[1].downcase.match(/asc|desc/)
app/controllers/friends_controller.rb:22
friends = User.where("name LIKE '%#{params[:name]}%' AND id NOT IN (#{ignore_ids.join(', ')})").order('name ASC')
friends = User.where('name LIKE ? AND id NOT IN (?)',"%#{params[:name]}%","#{ignore_ids.join(', ')}").order('name ASC')
app/controllers/sessions_controller.rb:6
user = User.find_by "login_id='#{params[:login_id]}' and pass='#{ Digest::MD5.hexdigest params[:pass] }'"
user = User.where("login_id=? and pass=?",params[:login_id],Digest::MD5.hexdigest(params[:pass])).take
【10.mal1c10us-ng1nxの除去】
Dockerfileではmal1c10us-ng1nxをベースにするように書かれているが、b4ckd00rなどが入っているため、
mal1c10us-ng1nxのベースであるubuntu:16.04を直接ベースとする。
また、nginxなど必要そうなパッケージに関してはDockerfileで直接インストール、自動起動設定する。
FROM ommadawn46/mal1c10us-ng1nx:latest => FROM ubuntu:16.04
vsftpd,fcgiwrap,netcatも入れているものの、サーバの要件によっては必要ない気がする。
というかvsftpdはAnonymousアクセスを許可しているため危うい気がし、netcatは簡単バックドア製造機にしか見えないので消す。
【11.Rubyのバージョン】
もとのDockerfileではRuby2.4を使用していたが、Ruby2.6が利用可能になっているため、
当該箇所をRuby2.6に書き換える
【12.gitとDocker build】
git for windowsをインストールし、[1-1]の手順に従いリポジトリを引っ張ってくる。
また、docker buildでDockerImageを生成できるか確認する。
なお、gemで一度だけエラーが発生し、その対策として以下を挿入した
RUN gem install nokogiri -v '1.10.2' --source 'https://rubygems.org/'
しかし消しても動作したため、もともと記述もなかったことを考慮し、削除。
rc.localは最新のUbuntuでは起動時に実行されないようで、
rc-local.serviceを登録しsystemdから実行されるようにする。
なお、ファイルはUTF-8NかつLFじゃないとエラーが頻発する。
Tera Padから編集してエラーが発生し、苦労した。
【13.MailHogについて】
MailHogは最新版を引っ張ってきているため、
おそらく脆弱性はない気がする
ただしCVEやJVNで検索をかけたのち、
badsns2019独自の設定、特にパーミッションを精査する予定
追記:資料から関係ないらしいことが判明。優先度最低へ
【14.MD5の廃止とSHA256の導入】
MD5の使用箇所について、GitHubの検索機能より
sessions_controller.rb:6
user.rb:16
index.html:25
password_reset_controller.rb:17
の4箇所であることがわかった。
SHA256を使用するにはDigest::MD5.hexdigestからDigest::SHA256.hexdigestとする。
また、それに伴い文字列のサイズが変化するため、データベースの構成を確認する。
schema.rbより、userテーブルのpassカラムはt.stringとして宣言されていることがわかった。そのため変更不要を確認。
なおMD5は16byte(ダイジェスト32文字),SHA256は32byte(ダイジェスト64文字)となる。
データベースの初期データはbad_sns_production.sqlに記述されているが、これはMD5のものなので、SHA256に変更する。
雑なパスワードはGoogle検索でもとの文字列が簡単にわかるので、できるだけ再現する(hash.txt参照)。
また、ソルトとして雑にlogin_idを使用して、SHA256(SHA256(pass)+SHA256(login_id))をパスワードとして保存する
sessions_controller.rb:6
user = User.where("login_id=? and pass=?",params[:login_id],Digest::SHA256.hexdigest(Digest::SHA256.hexdigest(params[:pass])+Digest::SHA256.hexdigest(params[:login_id]))).take
user.rb:16
self.pass = Digest::SHA256.hexdigest (Digest::SHA256.hexdigest(self.pass)+Digest::SHA256.hexdigest(self.login_id))
password_reset_controller.rb:17
user.update(pass: Digest::SHA256.hexdigest(Digest::SHA256.hexdigest(params[:pass])+Digest::SHA256.hexdigest(user.login_id)))
【15.リセットトークンの修正】
リセットトークンの発行はpassword_reset_controller.rbより、[id, login_id, name, email]から生成されている。
secret_key_baseは秘密であると仮定しても、少なくとも以下の3点が問題となる。
・毎回同じリセットトークンが生成されること
・有効期限が指定されていること
・暗号化にECBモードが指定されていること
上2点に関してはデータベースのUserテーブルに時刻情報を格納することが単純な解決策となるが、
提出期限まで8時間程度しかない現状でデータベースがからむ大規模な修正はしたくないので後回し。
ECBモードからCBCモードへの変更は2箇所の修正のみで完了するため、CBCモードを使用するように修正する。
【16.vulsとbrakemanによる再スキャン】
vulsによるスキャン結果は--ignore-unfixedで0件となったので良しとする。
brakemanについてはSQLインジェクションの見逃しが発覚
friends_controller.rb:17
friends = User.where("id IN (#{friend_ids.join(', ')})").order(order)
friends = User.where("id IN (?)","#{friend_ids.join(', ')}").order(order)
friends=ついてはなぜか動作しなかった。
等価に見えるのになぜかプレースホルダーが動かん。
ロールバック。
users_controller.rb:22
send_data File.read "#{Rails.root}/public/icons/#{user[:icon_file_name]}", disposition: 'inline'
send_data File.read "#{Rails.root}/public/icons/#{Shellwords.escape(user[:icon_file_name])}", disposition: 'inline'
feeds_controller.rb:30
img.write "#{Rails.root}/public/images/#{image_file_name}"
img.write "#{Rails.root}/public/images/#{Shellwords.escape(image_file_name)}"
feeds = Feed.joins(:user).where("user_id IN (#{scope_user_ids.join(', ')})").select('feeds.*,users.name').order('id DESC').limit(30)
feeds = Feed.joins(:user).where("user_id IN (?)","#{scope_user_ids.join(', ')}").select('feeds.*,users.name').order('id DESC').limit(30)
feeds = Feed.joins(:user).where("user_id IN (#{scope_user_ids.join(', ')}) and feeds.id > ?", params[:id].to_i).select('feeds.*,users.name').order('id DESC')
feeds = Feed.joins(:user).where("user_id IN (?) and feeds.id > ?","#{scope_user_ids.join(', ')}", params[:id].to_i).select('feeds.*,users.name').order('id DESC')
feeds = Feed.joins(:user).where("user_id IN (#{scope_user_ids.join(', ')}) and feeds.id < ?", params[:id].to_i).select('feeds.*,users.name').order('id DESC').limit(30)
feeds = Feed.joins(:user).where("user_id IN (?) and feeds.id < ?","#{scope_user_ids.join(', ')}", params[:id].to_i).select('feeds.*,users.name').order('id DESC').limit(30)
上記の3つのfeed=についてはなぜか動作しなかった。
Rails触るの初めてだし精進が必要。
【X.PUT,DELETEメソッドのサポート】
curlにて検査したところ、OPTIONS,PUT,DELETEメソッドはサポートされていないようであることがわかった。
雑に調べたものの、怪しくなさそうな雰囲気なので後回し。
追記:password_resetにてPATCH,PUT、sessionsにてDELETE、その他でGET,POSTのサポートを確認
とりあえず後回し
【X.TLS対応】
秘密情報を扱う以上TLSを使いたいが、ドメインもない上に手間がかかるので後回し。
production.rbにconfig.force_ssl = trueを追記することで暗号化可能らしい。
【Z.のこり】
任意コード実行はbackdoor?
任意OSコマンドはopenやreadのコード?
他に漏洩はvsftpd?そういえば/friendsとかのAPIでpass含む一行まるまるjsonで返してるからBurpで全部見れたような...(時間切れ)
管理者に漏れるのは、平文パスワードをサーバ側で処理してるから?ブラウザ側でハッシュかけてから送れという話?でもなんか別にある気がする。
データベースの不正操作はSQLインジェクションな気がするが、他にもありそうな気がする。
他のユーザになりすますのはなんだろ。セッションハイジャック、というかtoken取得?JWTは知らないけれども、元にしてる情報から見た感じ毎回同じものが生成されそう?
サーバ上の任意のファイルが読み出せるのは権限関係?この手のものだとディレクトリトラバーサルもありそう
XSSについてはどこかわからない。投稿やDBからの読み出し、ユーザ情報やリンクなど確認場所が多そう。
そもそも文脈的にapplicationの鍵が取れる気がする。
あとはnginxとかのコンフィグや、Dockerfileで指定している権限周りとか
nginx-defaultより、末尾が.phpならPHPが実行されるようなので、不必要なら消しておきたい。おそらくOSコマンドインジェクションやアップロード系と組み合わせる?
ImageMagic(+GhostScript)もまだなにか隠れていそう
資料の『スコアシートについて』の部分、パスごとに得点項目があるのは、このパスに少なくとも1つは脆弱性がある...?
気になるけれども時間もないし、そもそも研究を進めないとやばいのでここで終了。
【Z.プレースホルダーについての勘違い】
feeds = Feed.joins(:user).where("user_id IN (#{scope_user_ids.join(', ')})").select('feeds.*,users.name').order('id DESC').limit(30)
feeds = Feed.joins(:user).where("user_id IN (?)","#{scope_user_ids.join(', ')}").select('feeds.*,users.name').order('id DESC').limit(30)
例えば上記の2つについて、実際に生成されるSQLを.to_sqlによって確認したところ、前者は 1,2,3 後者は '1,2,3' とされるため結果が異なることがわかった。
よく考えたらプレースホルダーなのでそれはそう。
というかlogin_idと勘違いしてインジェクションされるかと思ったけれども、
よく見たらただのidだしbad_sns_production.sqlに記述してある定義からしてidにはintegerしか入らないのでそこまで危険ではない...?
【Z.icon_file_nameの書き換え】
ユーザの登録時の処理はregister.jsより、icon_file_nameをサーバ側でSecureRandom.uuidをもとに生成したのち、
一度クライアント側に返し、それを再度クライアントが送信する形になっている。 /icons => クライアント => /users
このときクライアントが編集して送信することにより、OSコマンドインジェクションが可能となる可能性がある。
具体的にはusers_controller.rbでFile.readにそのまま渡しているところが危うい。ひとまずShellwords.escapeで対応。
また、schema.rb、user.rb、bad_sns_production.sqlを確認したところ長さ確認はしてないようにみえる。
DBの容量を無駄に逼迫させる攻撃が可能となる可能性があるため、各ファイルにて対応が必要。だが、未実施。
【Z.OWASP ZAPによる検査】
OWASP ZAPは使用したことがなかったため、簡単に使用してみた。
インストーラを起動し、Javaがないと怒られ、Javaをインストールし、また怒られ、オフライン版の64bitを入れ、その後入った。
手順としては[ツール]-[オプション]からローカルプロキシを選択し、PC側でもプロキシを合わせる。
次にbadsnsにアクセスすると左側の列にある[サイト]にbadsnsのURLが表示されるため、右クリックからコンテキストに追加。
その後、同様にURLを右クリックから[攻撃]-[動的スキャン]を選択。
その結果は下側の[アラート]タブに表示されるが、メニューバーの[レポート]からHTML形式で生成したほうが見やすい。
生成したものを../log/owasp_zap_default.htmlとして保存した。
なお、対象としたbadsnsは手を加える前の初期状態のものである。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment